From 65307fdcb0435345bc3d9cc0c98da94785bbce19 Mon Sep 17 00:00:00 2001 From: Valentin Shirshov Date: Thu, 28 Sep 2023 16:58:23 +0200 Subject: [PATCH] [DBInstance] Add support for AutomatedBackupReplication (#446) * [DBInstance] Add support for AutomatedBackupReplication * [DBInstance] Address comments and nits * [DBInstance] Address comments and nits in tests * Address more comments * Add missing exception --------- Co-authored-by: Valentin Shirshov --- aws-rds-dbinstance/aws-rds-dbinstance.json | 7 + aws-rds-dbinstance/docs/README.md | 12 ++ aws-rds-dbinstance/resource-role.yaml | 2 + .../amazon/rds/dbinstance/BaseHandlerStd.java | 85 ++++++++ .../rds/dbinstance/CallbackContext.java | 4 +- .../amazon/rds/dbinstance/CreateHandler.java | 22 ++ .../amazon/rds/dbinstance/Translator.java | 26 +++ .../amazon/rds/dbinstance/UpdateHandler.java | 24 +++ .../dbinstance/client/RdsClientProvider.java | 6 + .../dbinstance/util/ResourceModelHelper.java | 32 +++ .../rds/dbinstance/AbstractHandlerTest.java | 7 + .../rds/dbinstance/CreateHandlerTest.java | 65 ++++++ .../rds/dbinstance/UpdateHandlerTest.java | 83 +++++++ .../client/RdsClientProviderTest.java | 8 +- .../util/ResourceModelHelperTest.java | 202 ++++++++++++++++++ 15 files changed, 583 insertions(+), 2 deletions(-) diff --git a/aws-rds-dbinstance/aws-rds-dbinstance.json b/aws-rds-dbinstance/aws-rds-dbinstance.json index 563ca3a0b..91daa93f6 100644 --- a/aws-rds-dbinstance/aws-rds-dbinstance.json +++ b/aws-rds-dbinstance/aws-rds-dbinstance.json @@ -130,6 +130,10 @@ "type": "boolean", "description": "A value that indicates whether minor engine upgrades are applied automatically to the DB instance during the maintenance window. By default, minor engine upgrades are applied automatically." }, + "AutomaticBackupReplicationRegion": { + "type": "string", + "description": "Enables replication of automated backups to a different Amazon Web Services Region." + }, "AvailabilityZone": { "type": "string", "description": "The Availability Zone (AZ) where the database will be created. For information on AWS Regions and Availability Zones." @@ -578,6 +582,7 @@ "rds:RebootDBInstance", "rds:RestoreDBInstanceFromDBSnapshot", "rds:RestoreDBInstanceToPointInTime", + "rds:StartDBInstanceAutomatedBackupsReplication", "secretsmanager:CreateSecret", "secretsmanager:TagResource" ], @@ -622,6 +627,8 @@ "rds:RebootDBInstance", "rds:RemoveRoleFromDBInstance", "rds:RemoveTagsFromResource", + "rds:StartDBInstanceAutomatedBackupsReplication", + "rds:StopDBInstanceAutomatedBackupsReplication", "secretsmanager:CreateSecret", "secretsmanager:TagResource" ], diff --git a/aws-rds-dbinstance/docs/README.md b/aws-rds-dbinstance/docs/README.md index bb08b273c..b6c71d7bf 100644 --- a/aws-rds-dbinstance/docs/README.md +++ b/aws-rds-dbinstance/docs/README.md @@ -16,6 +16,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy "AllowMajorVersionUpgrade" : Boolean, "AssociatedRoles" : [ DBInstanceRole, ... ], "AutoMinorVersionUpgrade" : Boolean, + "AutomaticBackupReplicationRegion" : String, "AvailabilityZone" : String, "BackupRetentionPeriod" : Integer, "CACertificateIdentifier" : String, @@ -100,6 +101,7 @@ Properties: AssociatedRoles: - DBInstanceRole AutoMinorVersionUpgrade: Boolean + AutomaticBackupReplicationRegion: String AvailabilityZone: String BackupRetentionPeriod: Integer CACertificateIdentifier: String @@ -222,6 +224,16 @@ _Type_: Boolean _Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) +#### AutomaticBackupReplicationRegion + +Enables replication of automated backups to a different Amazon Web Services Region. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + #### AvailabilityZone The Availability Zone (AZ) where the database will be created. For information on AWS Regions and Availability Zones. diff --git a/aws-rds-dbinstance/resource-role.yaml b/aws-rds-dbinstance/resource-role.yaml index abb3a96d6..7afe823f6 100644 --- a/aws-rds-dbinstance/resource-role.yaml +++ b/aws-rds-dbinstance/resource-role.yaml @@ -63,6 +63,8 @@ Resources: - "rds:RemoveTagsFromResource" - "rds:RestoreDBInstanceFromDBSnapshot" - "rds:RestoreDBInstanceToPointInTime" + - "rds:StartDBInstanceAutomatedBackupsReplication" + - "rds:StopDBInstanceAutomatedBackupsReplication" - "secretsmanager:CreateSecret" - "secretsmanager:TagResource" Resource: "*" diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java index 4ca848d96..0f0a43191 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java @@ -15,6 +15,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.amazonaws.arn.Arn; import org.apache.commons.lang3.BooleanUtils; import com.amazonaws.util.CollectionUtils; @@ -56,6 +57,7 @@ import software.amazon.awssdk.services.rds.model.InstanceQuotaExceededException; import software.amazon.awssdk.services.rds.model.InsufficientDbInstanceCapacityException; import software.amazon.awssdk.services.rds.model.InvalidDbClusterStateException; +import software.amazon.awssdk.services.rds.model.InvalidDbInstanceAutomatedBackupStateException; import software.amazon.awssdk.services.rds.model.InvalidDbInstanceStateException; import software.amazon.awssdk.services.rds.model.InvalidDbSecurityGroupStateException; import software.amazon.awssdk.services.rds.model.InvalidDbSnapshotStateException; @@ -90,6 +92,7 @@ import software.amazon.rds.common.handler.Events; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.HandlerMethod; +import software.amazon.rds.common.handler.Probing; import software.amazon.rds.common.handler.Tagging; import software.amazon.rds.common.logging.LoggingProxyClient; import software.amazon.rds.common.logging.RequestLogger; @@ -295,6 +298,14 @@ public abstract class BaseHandlerStd extends BaseHandler { InvalidDbInstanceStateException.class) .build(); + protected static final ErrorRuleSet MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET = ErrorRuleSet + .extend(DEFAULT_DB_INSTANCE_ERROR_RULE_SET) + .withErrorClasses(ErrorStatus.ignore(OperationStatus.IN_PROGRESS), + InvalidDbInstanceAutomatedBackupStateException.class) + .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ServiceLimitExceeded), + DbInstanceAutomatedBackupQuotaExceededException.class) + .build(); + protected static final ErrorRuleSet MODIFY_DB_INSTANCE_ERROR_RULE_SET = ErrorRuleSet .extend(DEFAULT_DB_INSTANCE_ERROR_RULE_SET) .withErrorCodes(ErrorStatus.failWith(HandlerErrorCode.ResourceConflict), @@ -684,6 +695,28 @@ protected boolean isDBInstanceStabilizedAfterMutate( return isDBInstanceStabilizedAfterMutateResult; } + protected boolean isInstanceStabilizedAfterReplicationStop(final ProxyClient rdsProxyClient, + final ResourceModel model) { + final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); + + assertNoTerminalStatus(dbInstance); + return isDBInstanceAvailable(dbInstance) + && !dbInstance.hasDbInstanceAutomatedBackupsReplications(); + } + + protected boolean isInstanceStabilizedAfterReplicationStart(final ProxyClient rdsProxyClient, + final ResourceModel model) { + final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); + + assertNoTerminalStatus(dbInstance); + return isDBInstanceAvailable(dbInstance) + && dbInstance.hasDbInstanceAutomatedBackupsReplications() && + !dbInstance.dbInstanceAutomatedBackupsReplications().isEmpty() && + model.getAutomaticBackupReplicationRegion() + .equalsIgnoreCase( + Arn.fromString(dbInstance.dbInstanceAutomatedBackupsReplications().get(0).dbInstanceAutomatedBackupsArn()).getRegion()); + } + protected boolean isDBInstanceStabilizedAfterReboot( final ProxyClient rdsProxyClient, final ResourceModel model @@ -1053,4 +1086,56 @@ protected ProgressEvent versioned( } return methodVersions.get(apiVersion).invoke(proxy, rdsProxyClient.forVersion(apiVersion), progress, allTags); } + + protected ProgressEvent stopAutomaticBackupReplicationInRegion( + final String dbInstanceArn, + final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, + final ProxyClient sourceRegionClient, + final String region + ) { + final ProxyClient rdsClient = proxy.newProxy(() -> new RdsClientProvider().getClientForRegion(region)); + + return proxy.initiate("rds::stop-db-instance-automatic-backup-replication", rdsClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(resourceModel -> Translator.stopDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn)) + .backoffDelay(config.getBackoff()) + .makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2( + request, + rdsClient.client()::stopDBInstanceAutomatedBackupsReplication + )) + .stabilize((request, response, client, model, context) -> + isInstanceStabilizedAfterReplicationStop(sourceRegionClient, model)) + .handleError((request, exception, client, model, context) -> Commons.handleException( + ProgressEvent.progress(model, context), + exception, + MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET + )) + .progress(); + } + + protected ProgressEvent startAutomaticBackupReplicationInRegion( + final String dbInstanceArn, + final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, + final ProxyClient sourceRegionClient, + final String region + ) { + final ProxyClient rdsClient = proxy.newProxy(() -> new RdsClientProvider().getClientForRegion(region)); + + return proxy.initiate("rds::start-db-instance-automatic-backup-replication", rdsClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(resourceModel -> Translator.startDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn)) + .backoffDelay(config.getBackoff()) + .makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2( + request, + rdsClient.client()::startDBInstanceAutomatedBackupsReplication + )) + .stabilize((request, response, proxyInvocation, model, context) -> + isInstanceStabilizedAfterReplicationStart(sourceRegionClient, model)) + .handleError((request, exception, client, model, context) -> Commons.handleException( + ProgressEvent.progress(model, context), + exception, + MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET + )) + .progress(); + } } diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java index 30ab8691c..c7f57a094 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java @@ -21,9 +21,11 @@ public class CallbackContext extends StdCallbackContext implements TaggingContex private boolean storageAllocated; private boolean allocatingStorage; private boolean readReplicaPromoted; + private boolean automaticBackupReplicationStopped; + private boolean automaticBackupReplicationStarted; + private String dbInstanceArn; private TaggingContext taggingContext; - private Map timestamps; public CallbackContext() { diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java index eaccca7ed..0ffd93edc 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java @@ -3,12 +3,17 @@ import java.time.Instant; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; import org.apache.commons.lang3.BooleanUtils; import com.amazonaws.util.StringUtils; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBInstance; import software.amazon.awssdk.services.rds.model.DBSnapshot; import software.amazon.awssdk.services.rds.model.SourceType; import software.amazon.awssdk.utils.ImmutableMap; @@ -27,6 +32,7 @@ import software.amazon.rds.common.request.Validations; import software.amazon.rds.common.util.IdentifierFactory; import software.amazon.rds.dbinstance.client.ApiVersion; +import software.amazon.rds.dbinstance.client.RdsClientProvider; import software.amazon.rds.dbinstance.client.VersionedProxyClient; import software.amazon.rds.dbinstance.util.ResourceModelHelper; @@ -169,6 +175,22 @@ ApiVersion.DEFAULT, safeAddTags(this::createDbInstance) .then(progress -> Commons.execOnce(progress, () -> updateAssociatedRoles(proxy, rdsProxyClient.defaultClient(), progress, Collections.emptyList(), desiredRoles), CallbackContext::isUpdatedRoles, CallbackContext::setUpdatedRoles)) + .then(progress -> Commons.execOnce(progress, () -> { + if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState()) + && StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn())) { + final DBInstance dbInstance = fetchDBInstance(rdsProxyClient.defaultClient(), progress.getResourceModel()); + callbackContext.setDbInstanceArn(dbInstance.dbInstanceArn()); + } + return progress; + }, (m) -> !StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn()), (v, c) -> {})) + .then(progress -> Commons.execOnce(progress, () -> { + if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { + return startAutomaticBackupReplicationInRegion(callbackContext.getDbInstanceArn(), proxy, progress, rdsProxyClient.defaultClient(), + ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getDesiredResourceState())); + } + return progress; + }, + CallbackContext::isAutomaticBackupReplicationStarted, CallbackContext::setAutomaticBackupReplicationStarted)) .then(progress -> { model.setTags(Translator.translateTagsFromSdk(Tagging.translateTagsToSdk(allTags))); return Commons.reportResourceDrift( diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java index 51e28a74f..8f55acfad 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.amazonaws.arn.Arn; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -42,6 +43,8 @@ import software.amazon.awssdk.services.rds.model.RemoveRoleFromDbInstanceRequest; import software.amazon.awssdk.services.rds.model.RestoreDbInstanceFromDbSnapshotRequest; import software.amazon.awssdk.services.rds.model.RestoreDbInstanceToPointInTimeRequest; +import software.amazon.awssdk.services.rds.model.StartDbInstanceAutomatedBackupsReplicationRequest; +import software.amazon.awssdk.services.rds.model.StopDbInstanceAutomatedBackupsReplicationRequest; import software.amazon.awssdk.utils.StringUtils; import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.Tagging; @@ -705,6 +708,22 @@ public static DescribeDbEngineVersionsRequest describeDbEngineVersionsRequest( .build(); } + public static StartDbInstanceAutomatedBackupsReplicationRequest startDbInstanceAutomatedBackupsReplicationRequest( + final String dbInstanceArn + ) { + return StartDbInstanceAutomatedBackupsReplicationRequest.builder() + .sourceDBInstanceArn(dbInstanceArn) + .build(); + } + + public static StopDbInstanceAutomatedBackupsReplicationRequest stopDbInstanceAutomatedBackupsReplicationRequest( + final String dbInstanceArn + ) { + return StopDbInstanceAutomatedBackupsReplicationRequest.builder() + .sourceDBInstanceArn(dbInstanceArn) + .build(); + } + public static List translateDbInstancesFromSdk( final List dbInstances ) { @@ -764,9 +783,16 @@ public static ResourceModel.ResourceModelBuilder translateDbInstanceFromSdkBuild optionGroupName = dbInstance.optionGroupMemberships().get(0).optionGroupName(); } + String automatedReplicationRegion = null; + if (dbInstance.hasDbInstanceAutomatedBackupsReplications() && !dbInstance.dbInstanceAutomatedBackupsReplications().isEmpty()) { + automatedReplicationRegion = Arn.fromString(dbInstance.dbInstanceAutomatedBackupsReplications() + .get(0).dbInstanceAutomatedBackupsArn()).getRegion(); + } + return ResourceModel.builder() .allocatedStorage(allocatedStorage) .associatedRoles(translateAssociatedRolesFromSdk(dbInstance.associatedRoles())) + .automaticBackupReplicationRegion(automatedReplicationRegion) .autoMinorVersionUpgrade(dbInstance.autoMinorVersionUpgrade()) .availabilityZone(dbInstance.availabilityZone()) .backupRetentionPeriod(dbInstance.backupRetentionPeriod()) diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java index cd63b61c2..9b7577c90 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java @@ -151,6 +151,30 @@ protected ProgressEvent handleRequest( updateAssociatedRoles(proxy, rdsClient, progress, previousRoles, desiredRoles), CallbackContext::isUpdatedRoles, CallbackContext::setUpdatedRoles) ) + .then(progress -> Commons.execOnce(progress, () -> { + if ((ResourceModelHelper.shouldStopAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState()) + || ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) + && StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn())) { + final DBInstance dbInstance = fetchDBInstance(rdsProxyClient.defaultClient(), progress.getResourceModel()); + callbackContext.setDbInstanceArn(dbInstance.dbInstanceArn()); + } + return progress; + }, (m) -> !StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn()), (v, c) -> {})) + .then(progress -> Commons.execOnce(progress, () -> { + if (ResourceModelHelper.shouldStopAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { + return stopAutomaticBackupReplicationInRegion(callbackContext.getDbInstanceArn(), proxy, progress, rdsProxyClient.defaultClient(), + ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getPreviousResourceState())); + } + return progress;}, + CallbackContext::isAutomaticBackupReplicationStopped, CallbackContext::setAutomaticBackupReplicationStopped)) + .then(progress -> Commons.execOnce(progress, () -> { + if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { + return startAutomaticBackupReplicationInRegion(callbackContext.getDbInstanceArn(), proxy, progress, rdsProxyClient.defaultClient(), + ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getDesiredResourceState())); + } + return progress; + }, + CallbackContext::isAutomaticBackupReplicationStarted, CallbackContext::setAutomaticBackupReplicationStarted)) .then(progress -> updateTags(proxy, rdsClient, progress, previousTags, desiredTags)) .then(progress -> { final ResourceModel model = request.getDesiredResourceState(); diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java index ed7b90e86..83031b3f8 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java @@ -11,6 +11,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.RdsClientBuilder; import software.amazon.rds.common.client.BaseSdkClientProvider; @@ -54,4 +55,9 @@ public RdsClient getClient() { public RdsClient getClientForApiVersion(@NonNull final String apiVersion) { return setUserAgentAndApiVersion(setHttpClient(RdsClient.builder()), apiVersion).build(); } + + public RdsClient getClientForRegion(@NonNull final String region) { + final Region sdkRegion = Region.of(region); + return setUserAgent(setHttpClient(RdsClient.builder().region(sdkRegion))).build(); + } } diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java index 10e98aec0..666577b18 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java @@ -6,6 +6,9 @@ import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.rds.dbinstance.ResourceModel; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -106,4 +109,33 @@ public static boolean isReadReplicaPromotion(final ResourceModel previous, final return isDBClusterReadReplicaPromotion(previous, desired) || isDBInstanceReadReplicaPromotion(previous, desired); } + + public static boolean shouldStartAutomaticBackupReplication(final ResourceModel previous, final ResourceModel desired) { + final String previousRegion = getAutomaticBackupReplicationRegion(previous); + final String desiredRegion = getAutomaticBackupReplicationRegion(desired); + return !StringUtils.isNullOrEmpty(desiredRegion) && !desiredRegion.equalsIgnoreCase(previousRegion); + } + + public static boolean shouldStopAutomaticBackupReplication(final ResourceModel previous, final ResourceModel desired) { + final String previousRegion = getAutomaticBackupReplicationRegion(previous); + final String desiredRegion = getAutomaticBackupReplicationRegion(desired); + return !StringUtils.isNullOrEmpty(previousRegion) && !previousRegion.equalsIgnoreCase(desiredRegion); + } + + public static int getBackupRetentionPeriod(final ResourceModel model) { + if (model == null) { + return 0; + } + return Optional.ofNullable(model.getBackupRetentionPeriod()).orElse(0); + } + + public static String getAutomaticBackupReplicationRegion(final ResourceModel model) { + if (model == null) { + return null; + } + if (getBackupRetentionPeriod(model) == 0) { + return null; + } + return model.getAutomaticBackupReplicationRegion(); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java index da6d2c93a..bf715f8e9 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java @@ -192,6 +192,9 @@ public abstract class AbstractHandlerTest extends AbstractTestBase verify() { } }; } + + protected static String getAutomaticBackupArn(final String region) { + return String.format("arn:aws:rds:%s:1234567890:auto-backup:ab-test", region); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java index a0e8ad2db..68c6b3903 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java @@ -2,6 +2,7 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -14,6 +15,7 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -27,7 +29,9 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import com.google.common.collect.ImmutableList; @@ -51,6 +55,7 @@ import software.amazon.awssdk.services.rds.model.DBCluster; import software.amazon.awssdk.services.rds.model.DBClusterSnapshot; import software.amazon.awssdk.services.rds.model.DBInstance; +import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackupsReplication; import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackup; import software.amazon.awssdk.services.rds.model.DBSnapshot; import software.amazon.awssdk.services.rds.model.DbClusterNotFoundException; @@ -83,6 +88,7 @@ import software.amazon.awssdk.services.rds.model.RestoreDbInstanceFromDbSnapshotResponse; import software.amazon.awssdk.services.rds.model.RestoreDbInstanceToPointInTimeRequest; import software.amazon.awssdk.services.rds.model.RestoreDbInstanceToPointInTimeResponse; +import software.amazon.awssdk.services.rds.model.StartDbInstanceAutomatedBackupsReplicationRequest; import software.amazon.awssdk.services.rds.model.StorageTypeNotSupportedException; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -1981,6 +1987,65 @@ public void handleRequest_RestoreDBInstanceToPointInTime_Reboot_Success() { verify(rdsProxy.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class)); } + @Test + public void handleRequest_startAutomaticBackupReplication() { + final CallbackContext context = new CallbackContext(); + context.setCreated(true); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + context.setAddTagsComplete(true); + context.setAutomaticBackupReplicationStarted(false); + + proxy = Mockito.spy(proxy); + + final RdsClient crossRegionRdsClient = mock(RdsClient.class); + final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); + doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder().dbInstanceAutomatedBackupsReplications( + Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)).build())).build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) + .build(), + expectSuccess() + ); + + verify(crossRegionRdsProxy.client(), times(1)).startDBInstanceAutomatedBackupsReplication(any(StartDbInstanceAutomatedBackupsReplicationRequest.class)); + verify(crossRegionRdsProxy.client(), atLeastOnce()).serviceName(); + verifyNoMoreInteractions(crossRegionRdsProxy.client()); + verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + } + + @Test + public void handleRequest_noAutomaticBackupReplication() { + final CallbackContext context = new CallbackContext(); + context.setCreated(true); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + context.setAddTagsComplete(true); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder().dbInstanceAutomatedBackupsReplications( + Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)).build())).build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) + .backupRetentionPeriod(0) + .build(), + expectSuccess() + ); + + verify(rdsProxy.client(), times(1)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + } + @Test public void fetchEngineFromDBInstanceSnapshot() { final CallbackContext context = new CallbackContext(); diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java index 03f59fba7..53ed1845c 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java @@ -2,6 +2,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -15,6 +16,7 @@ import java.util.Collections; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -28,7 +30,9 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import com.google.common.collect.ImmutableList; @@ -50,6 +54,7 @@ import software.amazon.awssdk.services.rds.model.DBClusterMember; import software.amazon.awssdk.services.rds.model.DBEngineVersion; import software.amazon.awssdk.services.rds.model.DBInstance; +import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackupsReplication; import software.amazon.awssdk.services.rds.model.DBParameterGroup; import software.amazon.awssdk.services.rds.model.DBParameterGroupStatus; import software.amazon.awssdk.services.rds.model.DBSubnetGroup; @@ -78,6 +83,8 @@ import software.amazon.awssdk.services.rds.model.RemoveRoleFromDbInstanceResponse; import software.amazon.awssdk.services.rds.model.RemoveTagsFromResourceRequest; import software.amazon.awssdk.services.rds.model.RemoveTagsFromResourceResponse; +import software.amazon.awssdk.services.rds.model.StartDbInstanceAutomatedBackupsReplicationRequest; +import software.amazon.awssdk.services.rds.model.StopDbInstanceAutomatedBackupsReplicationRequest; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -1641,4 +1648,80 @@ public void handleRequest_ObserveFailureEvent() { verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); verify(rdsProxy.client(), times(2)).describeDBInstances(any(DescribeDbInstancesRequest.class)); } + + @Test + public void handleRequest_startAutomaticBackupReplication() { + final CallbackContext context = new CallbackContext(); + context.setCreated(true); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + context.setAddTagsComplete(true); + context.setAutomaticBackupReplicationStarted(false); + context.setAutomaticBackupReplicationStopped(true); + + proxy = Mockito.spy(proxy); + + final RdsClient crossRegionRdsClient = mock(RdsClient.class); + final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); + doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .dbInstanceAutomatedBackupsReplications(Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn(getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION_ALTER)).build())) + .build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) + .build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION_ALTER) + .build(), + expectSuccess() + ); + + verify(crossRegionRdsProxy.client(), times(1)).startDBInstanceAutomatedBackupsReplication(any(StartDbInstanceAutomatedBackupsReplicationRequest.class)); + verify(crossRegionRdsProxy.client(), atLeastOnce()).serviceName(); + verifyNoMoreInteractions(crossRegionRdsProxy.client()); + verifyAccessPermissions(crossRegionRdsProxy.client()); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + } + + @Test + public void handleRequest_stopAutomaticBackupReplication() { + final CallbackContext context = new CallbackContext(); + context.setCreated(true); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + context.setAddTagsComplete(true); + context.setAutomaticBackupReplicationStarted(true); + context.setAutomaticBackupReplicationStopped(false); + + proxy = Mockito.spy(proxy); + + final RdsClient crossRegionRdsClient = mock(RdsClient.class); + final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); + doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) + .build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION_ALTER) + .build(), + expectSuccess() + ); + + verify(crossRegionRdsProxy.client(), times(1)).stopDBInstanceAutomatedBackupsReplication(any(StopDbInstanceAutomatedBackupsReplicationRequest.class)); + verify(crossRegionRdsProxy.client(), atLeastOnce()).serviceName(); + verifyAccessPermissions(crossRegionRdsProxy.client()); + verifyNoMoreInteractions(crossRegionRdsProxy.client()); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/client/RdsClientProviderTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/client/RdsClientProviderTest.java index 40f03faf2..d84dd1899 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/client/RdsClientProviderTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/client/RdsClientProviderTest.java @@ -11,7 +11,6 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -98,4 +97,11 @@ public void test_getClientWithApiVersion_fullExec() throws IOException { final String userAgent = httpExecuteRequest.httpRequest().headers().get("User-Agent").get(0); Assertions.assertThat(userAgent).startsWith(SDK_CLIENT_USER_AGENT_PREFIX); } + + @Test + public void test_getClientForRegion() { + final RdsClient client = new RdsClientProvider().getClientForRegion("eu-west-1"); + Assertions.assertThat(client.serviceClientConfiguration().region().id()).isEqualTo("eu-west-1"); + Assertions.assertThat(client).isNotNull(); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java index ee9035d10..b4814246e 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java @@ -279,4 +279,206 @@ public void shouldUpdateAfterCreate_whenRestoreFromSqlServerSnapshotAndAuroraEng assertThat(ResourceModelHelper.shouldUpdateAfterCreate(model)).isTrue(); } + + @Test + public void getBackupRetentionPeriod_returnsZeroWhenNotSet() { + final ResourceModel model = ResourceModel.builder() + .build(); + + assertThat(ResourceModelHelper.getBackupRetentionPeriod(model)).isEqualTo(0); + } + + @Test + public void getBackupRetentionPeriod_returnsValueWhenSet() { + final ResourceModel model = ResourceModel.builder() + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.getBackupRetentionPeriod(model)).isEqualTo(10); + } + + @Test + public void getAutomaticBackupReplicationRegion_returnsNullWhenBackupReplicationIsZero() { + final ResourceModel model = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .build(); + + assertThat(ResourceModelHelper.getAutomaticBackupReplicationRegion(model)).isNull(); + } + + @Test + public void getAutomaticBackupReplicationRegion_returnsValueWhenBackupReplicationIsZero() { + final ResourceModel model = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.getAutomaticBackupReplicationRegion(model)).isEqualTo("eu-west-1"); + } + + @Test + public void shouldStartAutomaticBackupReplication_returnsFalseWhenRegionUnchanged() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(previous, desired)).isFalse(); + } + + @Test + public void shouldStartAutomaticBackupReplication_returnsTrueWhenRegionChanged() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-2") + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(previous, desired)).isTrue(); + } + + @Test + public void shouldStartAutomaticBackupReplication_returnsTrueWhenBackupRetentionPeriodChangedFromNullToValue() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(previous, desired)).isTrue(); + } + + @Test + public void shouldStartAutomaticBackupReplication_returnsFalseWhenBackupRetentionPeriodChangedFromOneToTwo() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(1) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(2) + .build(); + + assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(previous, desired)).isFalse(); + } + + @Test + public void shouldStartAutomaticBackupReplication_returnsTrueWhePreviousModelIsNull() { + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(2) + .build(); + + assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(null, desired)).isTrue(); + } + + @Test + public void shouldStartAutomaticBackupReplication_returnsFalseWhePreviousModelIsNullAndFeatureNotEnabled() { + final ResourceModel desired = ResourceModel.builder() + .backupRetentionPeriod(2) + .build(); + + assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(null, desired)).isFalse(); + } + + @Test + public void shouldStopAutomaticBackupReplication_returnsFalseWhenPreviousRegionIsNull() { + final ResourceModel previous = ResourceModel.builder() + .backupRetentionPeriod(10) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isFalse(); + } + + + @Test + public void shouldStopAutomaticBackupReplication_returnsFalseWhenRegionUnchanged() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isFalse(); + } + + @Test + public void shouldStopAutomaticBackupReplication_returnsTrueWhenRegionChanged() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-2") + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isTrue(); + } + + @Test + public void shouldStopAutomaticBackupReplication_returnsTrueWhenBackupRetentionPeriodChangedFromValueToNull() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(10) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .build(); + + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isTrue(); + } + + @Test + public void shouldStopAutomaticBackupReplication_returnsFalseWhenBackupRetentionPeriodChangedFromOneToTwo() { + final ResourceModel previous = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(1) + .build(); + + final ResourceModel desired = ResourceModel.builder() + .automaticBackupReplicationRegion("eu-west-1") + .backupRetentionPeriod(2) + .build(); + + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isFalse(); + } + + @Test + public void shouldStopAutomaticBackupReplication_returnsTrueWhenRegionSetFromValuToNull() { + final ResourceModel previous = ResourceModel.builder() + .backupRetentionPeriod(10) + .automaticBackupReplicationRegion("eu-west-1") + .build(); + + final ResourceModel desired = ResourceModel.builder() + .backupRetentionPeriod(10) + .build(); + + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isTrue(); + } }