From aa19d30328ed096f7f03f2eced8b0724a4854ca0 Mon Sep 17 00:00:00 2001 From: dbbh Date: Mon, 3 Jun 2024 00:16:05 +0000 Subject: [PATCH] [DBCluster] Support Local Write Forwarding --- aws-rds-dbcluster/aws-rds-dbcluster.json | 4 ++ aws-rds-dbcluster/docs/README.md | 12 ++++ .../amazon/rds/dbcluster/BaseHandlerStd.java | 16 +++++- .../amazon/rds/dbcluster/Translator.java | 16 ++++++ .../rds/dbcluster/BaseHandlerStdTest.java | 57 +++++++++++++++++++ .../amazon/rds/dbcluster/TranslatorTest.java | 39 +++++++++++++ 6 files changed, 142 insertions(+), 2 deletions(-) diff --git a/aws-rds-dbcluster/aws-rds-dbcluster.json b/aws-rds-dbcluster/aws-rds-dbcluster.json index 1df87ea52..892c620d5 100644 --- a/aws-rds-dbcluster/aws-rds-dbcluster.json +++ b/aws-rds-dbcluster/aws-rds-dbcluster.json @@ -128,6 +128,10 @@ "description": "A value that indicates whether to enable mapping of AWS Identity and Access Management (IAM) accounts to database accounts. By default, mapping is disabled.", "type": "boolean" }, + "EnableLocalWriteForwarding": { + "description": "Specifies whether read replicas can forward write operations to the writer DB instance in the DB cluster. By default, write operations aren't allowed on reader DB instances.", + "type": "boolean" + }, "Engine": { "description": "The name of the database engine to be used for this DB cluster. Valid Values: aurora (for MySQL 5.6-compatible Aurora), aurora-mysql (for MySQL 5.7-compatible Aurora), and aurora-postgresql", "type": "string" diff --git a/aws-rds-dbcluster/docs/README.md b/aws-rds-dbcluster/docs/README.md index 396395cbd..73a537359 100644 --- a/aws-rds-dbcluster/docs/README.md +++ b/aws-rds-dbcluster/docs/README.md @@ -35,6 +35,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy "EnableGlobalWriteForwarding" : Boolean, "EnableHttpEndpoint" : Boolean, "EnableIAMDatabaseAuthentication" : Boolean, + "EnableLocalWriteForwarding" : Boolean, "Engine" : String, "EngineLifecycleSupport" : String, "EngineMode" : String, @@ -103,6 +104,7 @@ Properties: EnableGlobalWriteForwarding: Boolean EnableHttpEndpoint: Boolean EnableIAMDatabaseAuthentication: Boolean + EnableLocalWriteForwarding: Boolean Engine: String EngineLifecycleSupport: String EngineMode: String @@ -382,6 +384,16 @@ _Type_: Boolean _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) +#### EnableLocalWriteForwarding + +Specifies whether read replicas can forward write operations to the writer DB instance in the DB cluster. By default, write operations aren't allowed on reader DB instances. + +_Required_: No + +_Type_: Boolean + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + #### Engine The name of the database engine to be used for this DB cluster. Valid Values: aurora (for MySQL 5.6-compatible Aurora), aurora-mysql (for MySQL 5.7-compatible Aurora), and aurora-postgresql diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java index 1d9e2072f..ece649218 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java @@ -55,6 +55,7 @@ import software.amazon.awssdk.services.rds.model.InvalidSubnetException; import software.amazon.awssdk.services.rds.model.InvalidVpcNetworkStateException; import software.amazon.awssdk.services.rds.model.KmsKeyNotAccessibleException; +import software.amazon.awssdk.services.rds.model.LocalWriteForwardingStatus; import software.amazon.awssdk.services.rds.model.NetworkTypeNotSupportedException; import software.amazon.awssdk.services.rds.model.SnapshotQuotaExceededException; import software.amazon.awssdk.services.rds.model.StorageQuotaExceededException; @@ -334,16 +335,22 @@ protected boolean isDBClusterStabilized( final boolean isNoPendingChangesResult = isNoPendingChanges(dbCluster); final boolean isMasterUserSecretStabilizedResult = isMasterUserSecretStabilized(dbCluster); final boolean isGlobalWriteForwardingStabilizedResult = isGlobalWriteForwardingStabilized(dbCluster); + final boolean isLocalWriteForwardingStabilizedResult = isLocalWriteForwardingStabilized(dbCluster); requestLogger.log(String.format("isDbClusterStabilized: %b", isDBClusterStabilizedResult), ImmutableMap.of("isDbClusterAvailable", isDBClusterStabilizedResult, "isNoPendingChanges", isNoPendingChangesResult, "isMasterUserSecretStabilized", isMasterUserSecretStabilizedResult, - "isGlobalWriteForwardingStabilized", isGlobalWriteForwardingStabilizedResult), + "isGlobalWriteForwardingStabilized", isGlobalWriteForwardingStabilizedResult, + "isLocalWriteForwardingStabilized", isLocalWriteForwardingStabilizedResult), ImmutableMap.of("Description", "isDBClusterStabilized method will be repeatedly" + " called with a backoff mechanism after the modify call until it returns true. This" + " process will continue until all included flags are true.")); - return isDBClusterStabilizedResult && isNoPendingChangesResult && isMasterUserSecretStabilizedResult && isGlobalWriteForwardingStabilizedResult; + return isDBClusterStabilizedResult && + isNoPendingChangesResult && + isMasterUserSecretStabilizedResult && + isGlobalWriteForwardingStabilizedResult && + isLocalWriteForwardingStabilizedResult; } private void resourceStabilizationTime(final CallbackContext context) { @@ -369,6 +376,11 @@ protected static boolean isGlobalWriteForwardingStabilized(DBCluster dbCluster) dbCluster.globalWriteForwardingStatus() != WriteForwardingStatus.DISABLING); } + protected static boolean isLocalWriteForwardingStabilized(DBCluster dbCluster) { + return (dbCluster.localWriteForwardingStatus() != LocalWriteForwardingStatus.ENABLING && + dbCluster.localWriteForwardingStatus() != LocalWriteForwardingStatus.DISABLING); + } + protected boolean isClusterRemovedFromGlobalCluster( final ProxyClient proxyClient, final String previousGlobalClusterIdentifier, diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java index 9a2cb59e0..74b94ab0e 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java @@ -30,6 +30,7 @@ import software.amazon.awssdk.services.rds.model.DescribeGlobalClustersRequest; import software.amazon.awssdk.services.rds.model.DisableHttpEndpointRequest; import software.amazon.awssdk.services.rds.model.EnableHttpEndpointRequest; +import software.amazon.awssdk.services.rds.model.LocalWriteForwardingStatus; import software.amazon.awssdk.services.rds.model.ModifyDbClusterRequest; import software.amazon.awssdk.services.rds.model.RebootDbInstanceRequest; import software.amazon.awssdk.services.rds.model.RemoveFromGlobalClusterRequest; @@ -68,6 +69,7 @@ static CreateDbClusterRequest createDbClusterRequest( .enableGlobalWriteForwarding(model.getEnableGlobalWriteForwarding()) .enableHttpEndpoint(model.getEnableHttpEndpoint()) .enableIAMDatabaseAuthentication(model.getEnableIAMDatabaseAuthentication()) + .enableLocalWriteForwarding(model.getEnableLocalWriteForwarding()) .enablePerformanceInsights(model.getPerformanceInsightsEnabled()) .engine(model.getEngine()) .engineMode(model.getEngineMode()) @@ -219,6 +221,7 @@ static ModifyDbClusterRequest modifyDbClusterAfterCreateRequest(final ResourceMo .domain(desiredModel.getDomain()) .domainIAMRoleName(desiredModel.getDomainIAMRoleName()) .enableGlobalWriteForwarding(desiredModel.getEnableGlobalWriteForwarding()) + .enableLocalWriteForwarding(desiredModel.getEnableLocalWriteForwarding()) .enablePerformanceInsights(desiredModel.getPerformanceInsightsEnabled()) .iops(desiredModel.getIops()) .masterUserPassword(desiredModel.getMasterUserPassword()) @@ -272,6 +275,7 @@ static ModifyDbClusterRequest modifyDbClusterRequest( .domainIAMRoleName(desiredModel.getDomainIAMRoleName()) .enableGlobalWriteForwarding(desiredModel.getEnableGlobalWriteForwarding()) .enableIAMDatabaseAuthentication(diff(previousModel.getEnableIAMDatabaseAuthentication(), desiredModel.getEnableIAMDatabaseAuthentication())) + .enableLocalWriteForwarding(desiredModel.getEnableLocalWriteForwarding()) .enablePerformanceInsights(desiredModel.getPerformanceInsightsEnabled()) .iops(desiredModel.getIops()) .masterUserPassword(diff(previousModel.getMasterUserPassword(), desiredModel.getMasterUserPassword())) @@ -432,6 +436,17 @@ static Set translateTagsFromSdk( .collect(Collectors.toSet()); } + static boolean translateLocalWriteForwardingStatus(final LocalWriteForwardingStatus status) { + /* + * LocalWriteForwarding reports status as an enum rather than a boolean. + * CFN stabilization requires a boolean value to stabilize. + * This method projects the status into ENABLED X DISABLED. + * Both ENABLING and DISABLING states are stabilized on and are considered transient. + */ + return status == LocalWriteForwardingStatus.REQUESTED || + status == LocalWriteForwardingStatus.ENABLED; + } + static software.amazon.awssdk.services.rds.model.ServerlessV2ScalingConfiguration translateServerlessV2ScalingConfiguration( final ServerlessV2ScalingConfiguration serverlessV2ScalingConfiguration ) { @@ -542,6 +557,7 @@ public static ResourceModel translateDbClusterFromSdk( .enableGlobalWriteForwarding(dbCluster.globalWriteForwardingRequested()) .enableHttpEndpoint(dbCluster.httpEndpointEnabled()) .enableIAMDatabaseAuthentication(dbCluster.iamDatabaseAuthenticationEnabled()) + .enableLocalWriteForwarding(translateLocalWriteForwardingStatus(dbCluster.localWriteForwardingStatus())) .endpoint( Endpoint.builder() .address(dbCluster.endpoint()) diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/BaseHandlerStdTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/BaseHandlerStdTest.java index db045fba3..fd1eee678 100644 --- a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/BaseHandlerStdTest.java +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/BaseHandlerStdTest.java @@ -8,6 +8,7 @@ import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.LocalWriteForwardingStatus; import software.amazon.awssdk.services.rds.model.MasterUserSecret; import software.amazon.awssdk.services.rds.model.WriteForwardingStatus; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -135,6 +136,62 @@ void isGlobalWriteForwardingStabilized_globalWriteForwardingDisabling() { )).isFalse(); } + @Test + void isLocalWriteForwardingStabilized_localWriteForwardingNotRequested() { + Assertions.assertThat(BaseHandlerStd.isLocalWriteForwardingStabilized( + DBCluster.builder() + .build() + )).isTrue(); + } + + @Test + void isLocalWriteForwardingStabilized_localWriteForwardingRequested() { + Assertions.assertThat(BaseHandlerStd.isLocalWriteForwardingStabilized( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.REQUESTED) + .build() + )).isTrue(); + } + + @Test + void isLocalWriteForwardingStabilized_localWriteForwardingEnabled() { + Assertions.assertThat(BaseHandlerStd.isLocalWriteForwardingStabilized( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.ENABLED) + .build() + )).isTrue(); + } + + @Test + void isLocalWriteForwardingStabilized_localWriteForwardingEnabling() { + Assertions.assertThat(BaseHandlerStd.isLocalWriteForwardingStabilized( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.ENABLING) + .build() + )).isFalse(); + } + + @Test + void isLocalWriteForwardingStabilized_localWriteForwardingDisabled() { + // LocalWriteForwarding status will not enable until a reader is requested by customer + // This prevents customers from creating a stack with only primary and setting the property + // As WS does not validate this parameter the stack will wait on stabilization until timeout. + Assertions.assertThat(BaseHandlerStd.isLocalWriteForwardingStabilized( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.DISABLED) + .build() + )).isTrue(); + } + + @Test + void isLocalWriteForwardingStabilized_localWriteForwardingDisabling() { + Assertions.assertThat(BaseHandlerStd.isLocalWriteForwardingStabilized( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.DISABLING) + .build() + )).isFalse(); + } + @Test void validateRequest_BlankRegionIsAccepted() { diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java index bbb5a9cd7..1711bd835 100644 --- a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java @@ -13,6 +13,7 @@ import software.amazon.awssdk.services.rds.model.CreateDbClusterRequest; import software.amazon.awssdk.services.rds.model.DBCluster; import software.amazon.awssdk.services.rds.model.DomainMembership; +import software.amazon.awssdk.services.rds.model.LocalWriteForwardingStatus; import software.amazon.awssdk.services.rds.model.ModifyDbClusterRequest; import software.amazon.awssdk.services.rds.model.RestoreDbClusterFromSnapshotRequest; import software.amazon.awssdk.services.rds.model.RestoreDbClusterToPointInTimeRequest; @@ -39,6 +40,14 @@ public void createDbClusterRequest_enableGlobalWriteForwarding() { assertThat(request.enableGlobalWriteForwarding()).isEqualTo(Boolean.TRUE); } + @Test + public void createDbClusterRequest_enableLocalWriteForwarding() { + final ResourceModel model = RESOURCE_MODEL.toBuilder().enableLocalWriteForwarding(true).build(); + + final CreateDbClusterRequest request = Translator.createDbClusterRequest(model, Tagging.TagSet.emptySet()); + assertThat(request.enableLocalWriteForwarding()).isEqualTo(Boolean.TRUE); + } + @Test public void createDbClusterRequest_storageTypeAndIops_shouldBeSet() { final ResourceModel model = ResourceModel.builder() @@ -567,6 +576,36 @@ public void translateDbCluterFromSdk_useProvidedStorageType() { assertThat(model.getStorageType()).isEqualTo(STORAGE_TYPE_AURORA_IOPT1); } + @Test + public void translateDbCluterFromSdk_translateLocalWriteForwardingStatusRequested() { + final ResourceModel model = Translator.translateDbClusterFromSdk( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.REQUESTED) + .build() + ); + assertThat(model.getEnableLocalWriteForwarding()).isTrue(); + } + + @Test + public void translateDbCluterFromSdk_translateLocalWriteForwardingStatusEnabling() { + final ResourceModel model = Translator.translateDbClusterFromSdk( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.ENABLING) + .build() + ); + assertThat(model.getEnableLocalWriteForwarding()).isFalse(); + } + + @Test + public void translateDbCluterFromSdk_translateLocalWriteForwardingStatusDisabling() { + final ResourceModel model = Translator.translateDbClusterFromSdk( + DBCluster.builder() + .localWriteForwardingStatus(LocalWriteForwardingStatus.DISABLING) + .build() + ); + assertThat(model.getEnableLocalWriteForwarding()).isFalse(); + } + @Override protected BaseHandlerStd getHandler() { return null;