diff --git a/.gitignore b/.gitignore
index a6d02c52d..797bc2cee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,5 @@ env/
.gradle
/gradle/
/wrapper/
+# unit test artifacts
+aws-rds-cfn-common/src/main/resources/version.properties
diff --git a/README.md b/README.md
index 3e96cc66f..823530d1d 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,51 @@ The CloudFormation Resource Provider Package For Amazon Relational Database Serv
## License
This library is licensed under the Apache 2.0 License.
+
+### Generate testsAccountsConfig.yml for contract tests
+
+See [Uluru wiki](https://w.amazon.com/bin/view/AWS/CloudFormation/Teams/ProviderEx/RP-Framework/Projects/UluruContractTests#HCanIrunCTv2inpipelineusingmyownaccounts3F)
+
+Uluru allows service teams to run contract tests on their own accounts. This way, the test process is completely visible
+to the service team -- any errors can be easily debugged in Step Functions (instead of S3), any stuck dependency stacks
+can be freely removed and retried, and contract tests can reuse the same prefab resources as integration tests.
+
+File generation is only needed if: 1) RDS adds a new control plane region, 2) RDS adds a new CFN resource
+
+1. (One-time) Install jq and yq
+ ```
+ brew install jq yq
+ ```
+2. Run command to generate testsAccountsConfig.yml and copy the generated file to all projects' **contract-tests-artifacts** directories
+ ```
+ brazil-build generateTestAccountsConfig
+ ```
+3. Examine `git diff` to make sure the changes are expected
+4. CR the changes
+
+## IntelliJ Setup
+
+As long as you are using the latest BlackCaiman, it should "just work".
+1. Run `brazil-build` once at the root level.
+1. Open it just like any other Java package.
+1. Use the menu Brazil -> Sync from workspace (Enhanced)
+
+It's using the [Enhanced Sync from Workspace](https://builderhub.corp.amazon.com/docs/black-caiman/user-guide/enhanced-sync-from-workspace.html#override) so you must use
+a BlackCaiman version 2023.3.186.0.2023.2 or more recent.
+
+If you are adding a new resource type, you must edit the file `.bemol.toml` to add the project source and test files.
+You can simply copy an existing example.
+
+### Running unit tests
+
+It should just work. The unit nest needs "aws-rds-RESOURCE/target/schema" specifically in the classpath (handled by the bemol config file).
+
+### My local state is really strange!
+
+First make sure all your changes are saved/committed. There is no going back.
+
+There are many files that are in .gitignore. To truly clean your workspace, run the following:
+```
+brazil ws clean
+git clean -dfx
+```
diff --git a/aws-rds-cfn-common/pom.xml b/aws-rds-cfn-common/pom.xml
index aaab71632..15bb5b571 100644
--- a/aws-rds-cfn-common/pom.xml
+++ b/aws-rds-cfn-common/pom.xml
@@ -28,12 +28,12 @@
software.amazon.awssdk
utils
- 2.24.13
+ 2.25.12
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
software.amazon.cloudformation
diff --git a/aws-rds-customdbengineversion/docs/README.md b/aws-rds-customdbengineversion/docs/README.md
index 2b80e9ceb..e9ef231fb 100644
--- a/aws-rds-customdbengineversion/docs/README.md
+++ b/aws-rds-customdbengineversion/docs/README.md
@@ -160,7 +160,7 @@ _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormati
#### UseAwsProvidedLatestImage
-A value that indicates whether AWS provided latest image is applied automatically to the Custom Engine Version. By default, AWS provided latest image is applied automatically.
+A value that indicates whether AWS provided latest image is applied automatically to the Custom Engine Version. By default, AWS provided latest image is applied automatically. This value is only applied on create.
_Required_: No
diff --git a/aws-rds-customdbengineversion/pom.xml b/aws-rds-customdbengineversion/pom.xml
index 3f390ccf7..161ba5a28 100644
--- a/aws-rds-customdbengineversion/pom.xml
+++ b/aws-rds-customdbengineversion/pom.xml
@@ -23,7 +23,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
software.amazon.rds.common
diff --git a/aws-rds-dbcluster/docs/README.md b/aws-rds-dbcluster/docs/README.md
index a61c8ca76..9d1bd1741 100644
--- a/aws-rds-dbcluster/docs/README.md
+++ b/aws-rds-dbcluster/docs/README.md
@@ -750,3 +750,7 @@ Returns the Address
value.
#### SecretArn
Returns the SecretArn
value.
+
+#### StorageThroughput
+
+Specifies the storage throughput value for the DB cluster. This setting applies only to the gp3 storage type.
diff --git a/aws-rds-dbcluster/pom.xml b/aws-rds-dbcluster/pom.xml
index 6b745d7cc..d805bba43 100644
--- a/aws-rds-dbcluster/pom.xml
+++ b/aws-rds-dbcluster/pom.xml
@@ -30,7 +30,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
software.amazon.awssdk
diff --git a/aws-rds-dbclusterendpoint/pom.xml b/aws-rds-dbclusterendpoint/pom.xml
index f505bc59e..6ba1d4ede 100644
--- a/aws-rds-dbclusterendpoint/pom.xml
+++ b/aws-rds-dbclusterendpoint/pom.xml
@@ -30,7 +30,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
diff --git a/aws-rds-dbclusterparametergroup/pom.xml b/aws-rds-dbclusterparametergroup/pom.xml
index e36c8de10..77bbd4f29 100644
--- a/aws-rds-dbclusterparametergroup/pom.xml
+++ b/aws-rds-dbclusterparametergroup/pom.xml
@@ -30,7 +30,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
diff --git a/aws-rds-dbinstance/pom.xml b/aws-rds-dbinstance/pom.xml
index 604213fbb..ddf6145fa 100644
--- a/aws-rds-dbinstance/pom.xml
+++ b/aws-rds-dbinstance/pom.xml
@@ -33,7 +33,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
software.amazon.awssdk
diff --git a/aws-rds-dbparametergroup/pom.xml b/aws-rds-dbparametergroup/pom.xml
index c6e18e66d..782ec90d4 100644
--- a/aws-rds-dbparametergroup/pom.xml
+++ b/aws-rds-dbparametergroup/pom.xml
@@ -23,7 +23,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
diff --git a/aws-rds-dbsubnetgroup/pom.xml b/aws-rds-dbsubnetgroup/pom.xml
index 752efadb3..8191a119b 100644
--- a/aws-rds-dbsubnetgroup/pom.xml
+++ b/aws-rds-dbsubnetgroup/pom.xml
@@ -23,7 +23,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
diff --git a/aws-rds-eventsubscription/pom.xml b/aws-rds-eventsubscription/pom.xml
index 414f37f7b..35df84d44 100644
--- a/aws-rds-eventsubscription/pom.xml
+++ b/aws-rds-eventsubscription/pom.xml
@@ -29,7 +29,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
diff --git a/aws-rds-globalcluster/pom.xml b/aws-rds-globalcluster/pom.xml
index 7dae3a1e1..f9a58039b 100644
--- a/aws-rds-globalcluster/pom.xml
+++ b/aws-rds-globalcluster/pom.xml
@@ -28,7 +28,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
diff --git a/aws-rds-integration/aws-rds-integration.json b/aws-rds-integration/aws-rds-integration.json
index d40b46f68..f0d63bb62 100644
--- a/aws-rds-integration/aws-rds-integration.json
+++ b/aws-rds-integration/aws-rds-integration.json
@@ -9,6 +9,12 @@
"minLength": 1,
"maxLength": 64
},
+ "Description": {
+ "type": "string",
+ "description": "The description of the integration.",
+ "minLength": 1,
+ "maxLength": 1000
+ },
"Tags": {
"type": "array",
"maxItems": 50,
@@ -19,6 +25,13 @@
"$ref": "#/definitions/Tag"
}
},
+ "DataFilter": {
+ "type": "string",
+ "description": "The data filter for the integration.",
+ "minLength": 1,
+ "maxLength": 25600,
+ "pattern": "[a-zA-Z0-9_ \"\\\\\\-$,*.:?+\\/]*"
+ },
"SourceArn": {
"type": "string",
"description": "The Amazon Resource Name (ARN) of the Aurora DB cluster to use as the source for replication."
@@ -100,8 +113,7 @@
"/properties/SourceArn",
"/properties/TargetArn",
"/properties/KMSKeyId",
- "/properties/AdditionalEncryptionContext",
- "/properties/IntegrationName"
+ "/properties/AdditionalEncryptionContext"
],
"readOnlyProperties": [
"/properties/IntegrationArn",
@@ -130,7 +142,8 @@
"permissions": [
"rds:DescribeIntegrations",
"rds:AddTagsToResource",
- "rds:RemoveTagsFromResource"
+ "rds:RemoveTagsFromResource",
+ "rds:ModifyIntegration"
]
},
"delete": {
diff --git a/aws-rds-integration/docs/README.md b/aws-rds-integration/docs/README.md
index 7348ab0a1..a8e63e102 100644
--- a/aws-rds-integration/docs/README.md
+++ b/aws-rds-integration/docs/README.md
@@ -13,7 +13,9 @@ To declare this entity in your AWS CloudFormation template, use the following sy
"Type" : "AWS::RDS::Integration",
"Properties" : {
"IntegrationName" : String,
+ "Description" : String,
"Tags" : [ Tag, ... ],
+ "DataFilter" : String,
"SourceArn" : String,
"TargetArn" : String,
"KMSKeyId" : String,
@@ -28,8 +30,10 @@ To declare this entity in your AWS CloudFormation template, use the following sy
Type: AWS::RDS::Integration
Properties:
IntegrationName: String
+ Description: String
Tags:
- Tag
+ DataFilter: String
SourceArn: String
TargetArn: String
KMSKeyId: String
@@ -50,7 +54,21 @@ _Minimum Length_: 1
_Maximum Length_: 64
-_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Description
+
+The description of the integration.
+
+_Required_: No
+
+_Type_: String
+
+_Minimum Length_: 1
+
+_Maximum Length_: 1000
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
#### Tags
@@ -62,6 +80,22 @@ _Type_: List of Tag
_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+#### DataFilter
+
+The data filter for the integration.
+
+_Required_: No
+
+_Type_: String
+
+_Minimum Length_: 1
+
+_Maximum Length_: 25600
+
+_Pattern_: [a-zA-Z0-9_ "\\\-$,*.:?+\/]*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
#### SourceArn
The Amazon Resource Name (ARN) of the Aurora DB cluster to use as the source for replication.
diff --git a/aws-rds-integration/pom.xml b/aws-rds-integration/pom.xml
index e9557b9af..43cc7974f 100644
--- a/aws-rds-integration/pom.xml
+++ b/aws-rds-integration/pom.xml
@@ -22,7 +22,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12
diff --git a/aws-rds-integration/resource-role.yaml b/aws-rds-integration/resource-role.yaml
index 5caac3852..ad2a19f7e 100644
--- a/aws-rds-integration/resource-role.yaml
+++ b/aws-rds-integration/resource-role.yaml
@@ -36,6 +36,7 @@ Resources:
- "rds:CreateIntegration"
- "rds:DeleteIntegration"
- "rds:DescribeIntegrations"
+ - "rds:ModifyIntegration"
- "rds:RemoveTagsFromResource"
- "redshift:CreateInboundIntegration"
Resource: "*"
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java
index 61db52b31..bf46aad90 100644
--- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java
@@ -6,6 +6,7 @@
import software.amazon.awssdk.services.rds.model.IntegrationNotFoundException;
import software.amazon.awssdk.services.rds.model.IntegrationQuotaExceededException;
import software.amazon.awssdk.services.rds.model.IntegrationStatus;
+import software.amazon.awssdk.services.rds.model.InvalidIntegrationStateException;
import software.amazon.awssdk.services.rds.model.KmsKeyNotAccessibleException;
import software.amazon.awssdk.services.rds.model.Tag;
import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
@@ -30,13 +31,18 @@
import java.util.Optional;
public abstract class BaseHandlerStd extends BaseHandler {
- protected final static String INTEGRATION_NAME_CONFLICT_ERROR_MESSAGE = "Integration names must be unique within an account";
/** If this message is given with IntegrationConflictOperationFault, then it is a retriable error.
* This is thrown when the underlying Redshift cluster is busy with other operations, but they are always
- * transient, unless something is really wrong. */
+ * transient, unless something is really wrong.
+ * Applies to IntegrationConflictOperationException */
protected final static String INTEGRATION_RETRIABLE_CONFLICT_MESSAGE = "because another operation is in progress for the " +
"Amazon Redshift data warehouse specified by the Amazon Resource Name (ARN). " +
"Try again after the current operation completes.";
+ /**
+ * Applies to InvalidIntegrationStateException, similar to INTEGRATION_RETRIABLE_CONFLICT_MESSAGE
+ */
+ protected final static String INTEGRATION_RETRIABLE_CONFLICTING_STATE_MESSAGE = "because it is not in a valid state. " +
+ "Wait until the integration is in a valid state and try again.";
protected static final String STACK_NAME = "rds";
protected static final String RESOURCE_IDENTIFIER = "integration";
protected static final int MAX_LENGTH_INTEGRATION = 63;
@@ -61,23 +67,24 @@ public abstract class BaseHandlerStd extends BaseHandler {
.withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.NotFound),
IntegrationNotFoundException.class)
.withErrorClasses(ErrorStatus.conditional((e) -> {
- // this condition happens on create-integration.
- // it's a little strange that IntegrationConflictOperationException is thrown instead of AlreadyExists exception
- // we need to override the default error handling because in this case we need to tell CFN that it's an AlreadyExists.
- // otherwise,
String nonNullErrorMessage = Optional.ofNullable(e.getMessage()).orElse("");
- if (nonNullErrorMessage.contains(INTEGRATION_NAME_CONFLICT_ERROR_MESSAGE)) {
- return ErrorStatus.failWith(HandlerErrorCode.AlreadyExists);
- } else if (nonNullErrorMessage.contains(INTEGRATION_RETRIABLE_CONFLICT_MESSAGE)) {
+ if (nonNullErrorMessage.contains(INTEGRATION_RETRIABLE_CONFLICT_MESSAGE)) {
// this tells the cfn framework to come back in a bit.
return ErrorStatus.retry(CALLBACK_DELAY);
} else {
- // this shouldn't happen but it's a good fallback.
+ // it's unclear if it's safe to retry otherwise.
return ErrorStatus.failWith(HandlerErrorCode.ResourceConflict);
}
}), IntegrationConflictOperationException.class)
- .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ResourceConflict),
- IntegrationConflictOperationException.class)
+ .withErrorClasses(ErrorStatus.conditional((e) -> {
+ String nonNullErrorMessage = Optional.ofNullable(e.getMessage()).orElse("");
+ if (nonNullErrorMessage.contains(INTEGRATION_RETRIABLE_CONFLICTING_STATE_MESSAGE)) {
+ return ErrorStatus.retry(CALLBACK_DELAY);
+ } else {
+ // it's unclear if it's safe to retry otherwise.
+ return ErrorStatus.failWith(HandlerErrorCode.ResourceConflict);
+ }
+ }), InvalidIntegrationStateException.class)
.withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ServiceLimitExceeded),
IntegrationQuotaExceededException.class)
.withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.AccessDenied),
@@ -133,7 +140,7 @@ protected ProgressEvent handleRequest(
) {
this.requestLogger = requestLogger;
return handleRequest(proxy, proxyClient, request, context);
- };
+ }
/**
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java
index 1321afb0a..a6606d6b7 100644
--- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java
@@ -3,11 +3,14 @@
import com.amazonaws.util.StringUtils;
import software.amazon.awssdk.services.rds.RdsClient;
import software.amazon.awssdk.services.rds.model.IntegrationConflictOperationException;
+import software.amazon.awssdk.services.rds.model.InvalidIntegrationStateException;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.HandlerErrorCode;
import software.amazon.cloudformation.proxy.ProgressEvent;
import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import software.amazon.rds.common.error.ErrorRuleSet;
+import software.amazon.rds.common.error.ErrorStatus;
import software.amazon.rds.common.handler.Commons;
import software.amazon.rds.common.handler.HandlerConfig;
import software.amazon.rds.common.handler.Tagging;
@@ -25,6 +28,30 @@ public class CreateHandler extends BaseHandlerStd {
MAX_LENGTH_INTEGRATION
);
+ /** We cannot "retry" create API call if we do not know the primaryIdentifier
+ * (in this case, the integrationArn is the primaryIdentifier).
+ * The integration ARN is only vended on a successful create.
+ * A CFN createHandler MUST return a valid primaryIdentifier on the first lambda invocation.
+ * Since this is not possible on a failed create-integration call,
+ * we will just fail on a certain set of retriable conditions.
+ */
+ protected static final ErrorRuleSet CREATE_INTEGRATION_ERROR_RULE_SET = ErrorRuleSet
+ .extend(DEFAULT_INTEGRATION_ERROR_RULE_SET)
+ .withErrorClasses(ErrorStatus.conditional((e) -> {
+ String nonNullErrorMessage = Optional.ofNullable(e.getMessage()).orElse("");
+ // this condition happens on create-integration.
+ // it's a little strange that IntegrationConflictOperationException is thrown instead of AlreadyExists exception
+ // we need to override the default error handling because in this case we need to tell CFN that it's an AlreadyExists.
+ if (nonNullErrorMessage.contains(INTEGRATION_NAME_CONFLICT_ERROR_MESSAGE)) {
+ return ErrorStatus.failWith(HandlerErrorCode.AlreadyExists);
+ } else {
+ // we cannot retry for create
+ return ErrorStatus.failWith(HandlerErrorCode.ResourceConflict);
+ }
+ }), IntegrationConflictOperationException.class)
+ .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ResourceConflict), InvalidIntegrationStateException.class)
+ .build();
+
/** Default constructor w/ default backoff */
public CreateHandler() {
this(HandlerConfig.builder()
@@ -70,23 +97,11 @@ private ProgressEvent createIntegration(final Am
resourceModel.setIntegrationArn(createIntegrationResponse.integrationArn());
return isStabilized(resourceModel, proxyInvocation);
})
- .handleError((createRequest, exception, client, resourceModel, ctx) -> {
- if (IntegrationConflictOperationException.class.isAssignableFrom(exception.getClass())) {
- // it's a little strange that IntegrationConflictOperationException is thrown instead of AlreadyExists exception
- // we need to override the default error handling because in this case we need to tell CFN that it's an AlreadyExists.
- String nonNullErrorMessage = Optional.ofNullable(exception.getMessage()).orElse("");
- if (nonNullErrorMessage.contains(INTEGRATION_NAME_CONFLICT_ERROR_MESSAGE)) {
- return ProgressEvent.failed(null, null, HandlerErrorCode.AlreadyExists, exception.getMessage());
- } else if (nonNullErrorMessage.contains(INTEGRATION_RETRIABLE_CONFLICT_MESSAGE)) {
- return ProgressEvent.failed(null, null, HandlerErrorCode.ResourceConflict, exception.getMessage());
- }
- }
- return Commons.handleException(
- ProgressEvent.progress(resourceModel, ctx),
- exception,
- DEFAULT_INTEGRATION_ERROR_RULE_SET,
- requestLogger);
- })
+ .handleError((createRequest, exception, client, resourceModel, ctx) -> Commons.handleException(
+ ProgressEvent.progress(resourceModel, ctx),
+ exception,
+ CREATE_INTEGRATION_ERROR_RULE_SET,
+ requestLogger))
.progress();
}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/DeleteHandler.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/DeleteHandler.java
index 097e0bbc4..e296cabaa 100644
--- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/DeleteHandler.java
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/DeleteHandler.java
@@ -1,7 +1,6 @@
package software.amazon.rds.integration;
import software.amazon.awssdk.services.rds.RdsClient;
-import software.amazon.awssdk.services.rds.model.IntegrationConflictOperationException;
import software.amazon.awssdk.services.rds.model.IntegrationNotFoundException;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.ProgressEvent;
@@ -12,8 +11,6 @@
import software.amazon.rds.common.handler.Commons;
import software.amazon.rds.common.handler.HandlerConfig;
-import java.util.Optional;
-
public class DeleteHandler extends BaseHandlerStd {
// Currently, if you re-create an Integration within 500 seconds of deletion against the same cluster,
// The Integration may fail to create. Remove when the issue no longer exists.
@@ -45,26 +42,18 @@ protected ProgressEvent handleRequest(
.makeServiceCall((deleteIntegrationRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(deleteIntegrationRequest, proxyInvocation.client()::deleteIntegration))
.stabilize((deleteIntegrationRequest, deleteIntegrationResponse, proxyInvocation, model, context) ->
isDeleted(model, proxyInvocation))
- .handleError((deleteRequest, exception, client, resourceModel, ctx) -> {
- if (IntegrationConflictOperationException.class.isAssignableFrom(exception.getClass())) {
- String nonNullErrorMessage = Optional.ofNullable(exception.getMessage()).orElse("");
- if (nonNullErrorMessage.contains(INTEGRATION_RETRIABLE_CONFLICT_MESSAGE)) {
- // this tells the cfn framework to come back in a bit.
- return ProgressEvent.defaultInProgressHandler(ctx, CALLBACK_DELAY, resourceModel);
- }
- }
- return Commons.handleException(
- ProgressEvent.progress(resourceModel, ctx),
- exception,
- // if the integration is already deleted, this should be ignored,
- // but only once we started the deletion process
- ErrorRuleSet.extend(DEFAULT_INTEGRATION_ERROR_RULE_SET)
- .withErrorClasses(ErrorStatus.ignore(), IntegrationNotFoundException.class)
- .build(),
- requestLogger
- );
- }
- )
+ .handleError((deleteRequest, exception, client, resourceModel, ctx) -> Commons.handleException(
+ ProgressEvent.progress(resourceModel, ctx),
+ exception,
+ // if the integration is already deleted, this should be ignored,
+ // but only once, we started the deletion process
+ // see checkIfIntegrationExists() for the opposite case
+ ErrorRuleSet
+ .extend(DEFAULT_INTEGRATION_ERROR_RULE_SET)
+ .withErrorClasses(ErrorStatus.ignore(), IntegrationNotFoundException.class)
+ .build(),
+ requestLogger
+ ))
.progress()
.then((e) -> delay(e, POST_DELETION_DELAY_SEC))
.then((e) -> ProgressEvent.defaultSuccessHandler(null)));
@@ -74,7 +63,6 @@ private ProgressEvent checkIfIntegrationExists(f
final ResourceHandlerRequest request,
final CallbackContext callbackContext,
final ProxyClient proxyClient) {
- // it is part of the CFN contract that we return NotFound on DELETE.
return proxy.initiate("rds::delete-integration-check-exists", proxyClient, request.getDesiredResourceState(), callbackContext)
.translateToServiceRequest(Translator::describeIntegrationsRequest)
.backoffDelay(config.getBackoff())
@@ -82,6 +70,7 @@ private ProgressEvent checkIfIntegrationExists(f
.handleError((deleteRequest, exception, client, resourceModel, ctx) -> Commons.handleException(
ProgressEvent.progress(resourceModel, ctx),
exception,
+ // this is intentional. it is part of the CFN contract that we return NotFound on DELETE.
DEFAULT_INTEGRATION_ERROR_RULE_SET,
requestLogger
))
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/Translator.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/Translator.java
index 8edc6583a..407c40ce9 100644
--- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/Translator.java
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/Translator.java
@@ -1,11 +1,13 @@
package software.amazon.rds.integration;
import com.amazonaws.util.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
import software.amazon.awssdk.services.rds.model.CreateIntegrationRequest;
import software.amazon.awssdk.services.rds.model.DeleteIntegrationRequest;
import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
import software.amazon.awssdk.services.rds.model.Filter;
import software.amazon.awssdk.services.rds.model.Integration;
+import software.amazon.awssdk.services.rds.model.ModifyIntegrationRequest;
import software.amazon.rds.common.handler.Tagging;
import java.text.DateFormat;
@@ -18,6 +20,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
+import java.util.function.Function;
import java.util.stream.Collectors;
public class Translator {
@@ -40,7 +43,66 @@ public static CreateIntegrationRequest createIntegrationRequest(
.targetArn(model.getTargetArn())
.additionalEncryptionContext(model.getAdditionalEncryptionContext())
.tags(Tagging.translateTagsToSdk(tags))
+ .dataFilter(model.getDataFilter())
+ .description(model.getDescription())
.build();
+ }
+
+ /**
+ * For Update, do not update a field, if the previous value is not null, but the new value is null.
+ * This is consistent most other fields in other RDS resources.
+ * In the future, if we have more fields, we may need different logic such as resetting the value when
+ * a field changes to null.
+ */
+ public static boolean shouldModifyField(
+ ResourceModel previousModel,
+ ResourceModel desiredModel,
+ Function attributeGetter
+ ) {
+ String previousValue = attributeGetter.apply(previousModel);
+ String desiredValue = attributeGetter.apply(desiredModel);
+ if (StringUtils.equals(previousValue, desiredValue)) {
+ return false;
+ }
+ if (desiredValue == null) {
+ return false;
+ }
+ // this may change once we allow empty DataFilter or Description on the service-side.
+ if (desiredValue.isEmpty()) {
+ return false;
+ }
+ return true;
+ }
+
+ /** Generates the ModifyIntegrationRequest based on the previous and the desired models.
+ * Precondition: software.amazon.rds.integration.UpdateHandler#shouldModifyIntegration()
+ * should have returned true for the pair of models.
+ * */
+ public static ModifyIntegrationRequest modifyIntegrationRequest(
+ final ResourceModel previousModel,
+ final ResourceModel desiredModel
+ ) {
+ ModifyIntegrationRequest.Builder builder = ModifyIntegrationRequest.builder()
+ .integrationIdentifier(desiredModel.getIntegrationArn());
+
+ if (shouldModifyField(previousModel, desiredModel, ResourceModel::getIntegrationName)) {
+ // integration name can not be empty here, because we will populate it at the model level.
+ builder.integrationName(desiredModel.getIntegrationName());
+ }
+
+ if (shouldModifyField(previousModel, desiredModel, ResourceModel::getDescription)) {
+ // currently, due to a quirk, we cannot unset the description.
+ // so we will ignore the empty case
+ builder.description(desiredModel.getDescription());
+ }
+
+ if (shouldModifyField(previousModel, desiredModel, ResourceModel::getDataFilter)) {
+ // currently, due to a quirk, we cannot unset the description.
+ // so we will ignore the empty case
+ builder.dataFilter(desiredModel.getDataFilter());
+ }
+
+ return builder.build();
}
static DescribeIntegrationsRequest describeIntegrationsRequest(final ResourceModel model) {
@@ -108,6 +170,8 @@ static ResourceModel translateToModel(
.targetArn(integration.targetArn())
.kMSKeyId(integration.kmsKeyId())
.tags(translateTags(integration.tags()))
+ .dataFilter(integration.dataFilter())
+ .description(integration.description())
.additionalEncryptionContext(integration.additionalEncryptionContext())
.build();
}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/UpdateHandler.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/UpdateHandler.java
index eb171e377..9e491a0c2 100644
--- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/UpdateHandler.java
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/UpdateHandler.java
@@ -5,11 +5,14 @@
import software.amazon.cloudformation.proxy.ProgressEvent;
import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import software.amazon.rds.common.handler.Commons;
import software.amazon.rds.common.handler.HandlerConfig;
import software.amazon.rds.common.handler.Tagging;
import java.util.HashSet;
+import static software.amazon.rds.integration.Translator.shouldModifyField;
+
public class UpdateHandler extends BaseHandlerStd {
/** Default constructor w/ default backoff */
public UpdateHandler() {
@@ -31,6 +34,7 @@ protected ProgressEvent handleRequest(
) {
// Currently Integration resource only supports Tags update.
final ResourceModel desiredModel = request.getDesiredResourceState();
+ final ResourceModel previousModel = request.getPreviousResourceState();
final Tagging.TagSet previousTags = Tagging.TagSet.builder()
.systemTags(Tagging.translateTagsToSdk(request.getPreviousSystemTags()))
@@ -45,8 +49,39 @@ protected ProgressEvent handleRequest(
.build();
return ProgressEvent.progress(desiredModel, callbackContext)
+ .then(progress -> {
+ if (shouldModifyIntegration(previousModel, progress.getResourceModel())) {
+ return modifyIntegration(proxy, proxyClient, previousModel, progress);
+ }
+ return progress;
+ })
.then(progress -> updateTags(proxy, proxyClient, progress, previousTags, desiredTags))
.then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext));
}
+ private boolean shouldModifyIntegration(final ResourceModel previousModel, final ResourceModel desiredModel) {
+ return previousModel != null && (
+ shouldModifyField(previousModel, desiredModel, ResourceModel::getDescription) ||
+ shouldModifyField(previousModel, desiredModel, ResourceModel::getDataFilter) ||
+ shouldModifyField(previousModel, desiredModel, ResourceModel::getIntegrationName));
+ }
+
+ private ProgressEvent modifyIntegration(final AmazonWebServicesClientProxy proxy,
+ final ProxyClient proxyClient,
+ final ResourceModel previousModel,
+ final ProgressEvent progress) {
+ return proxy.initiate("rds::modify-integration", proxyClient, progress.getResourceModel(), progress.getCallbackContext())
+ .translateToServiceRequest((desiredModel) ->
+ Translator.modifyIntegrationRequest(previousModel, desiredModel))
+ .backoffDelay(config.getBackoff())
+ .makeServiceCall((modifyIntegrationRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(modifyIntegrationRequest, proxyInvocation.client()::modifyIntegration))
+ .stabilize((modifyIntegrationRequest, modifyIntegrationResponse, proxyInvocation, resourceModel, context) -> isStabilized(resourceModel, proxyInvocation))
+ .handleError((awsRequest, exception, client, resourceModel, context) -> Commons.handleException(
+ ProgressEvent.progress(resourceModel, context),
+ exception,
+ DEFAULT_INTEGRATION_ERROR_RULE_SET,
+ requestLogger)
+ )
+ .progress();
+ }
}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/AbstractHandlerTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/AbstractHandlerTest.java
index 4a5c82b18..1eecd1f9d 100644
--- a/aws-rds-integration/src/test/java/software/amazon/rds/integration/AbstractHandlerTest.java
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/AbstractHandlerTest.java
@@ -96,11 +96,16 @@ public abstract class AbstractHandlerTest extends AbstractTestBase ADDITIONAL_ENCRYPTION_CONTEXT = ImmutableMap.of("eck1", "ecv1", "eck2", "ecv2");
+ protected static final String DESCRIPTION = "integration description";
+ protected static final String DATA_FILTER = "include: *.*";
+ protected static final String DESCRIPTION_ALTER = "integration description 2";
+ protected static final String DATA_FILTER_ALTER = "include: ";
static final Integration INTEGRATION_ACTIVE = Integration.builder()
.integrationArn(INTEGRATION_ARN)
@@ -112,6 +117,8 @@ public abstract class AbstractHandlerTest extends AbstractTestBaseargThat(req -> {
- // TODO verify the content
- return true;
- })
+ ArgumentMatchers. argThat(req -> Objects.equals(req.integrationName(), INTEGRATION_NAME) &&
+ Objects.equals(req.sourceArn(), SOURCE_ARN) &&
+ Objects.equals(req.targetArn(), TARGET_ARN) &&
+ Objects.equals(req.description(), DESCRIPTION) &&
+ Objects.equals(req.dataFilter(), DATA_FILTER))
);
verify(rdsProxy.client(), times(3)).describeIntegrations(
ArgumentMatchers.argThat(req ->
@@ -174,10 +175,11 @@ public void handleRequest_CreateIntegration_withTerminalFailureState_returnFailu
).isInstanceOf(CfnNotStabilizedException.class);
verify(rdsProxy.client(), times(1)).createIntegration(
- ArgumentMatchers. argThat(req -> {
- // TODO verify the content
- return true;
- })
+ ArgumentMatchers. argThat(req -> Objects.equals(req.integrationName(), INTEGRATION_NAME) &&
+ Objects.equals(req.sourceArn(), SOURCE_ARN) &&
+ Objects.equals(req.targetArn(), TARGET_ARN) &&
+ Objects.equals(req.description(), DESCRIPTION) &&
+ Objects.equals(req.dataFilter(), DATA_FILTER))
);
verify(rdsProxy.client(), times(2)).describeIntegrations(
@@ -202,10 +204,37 @@ public void handleRequest_CreateIntegration_withIntegrationAlreadyExistsExceptio
);
verify(rdsProxy.client(), times(1)).createIntegration(
- ArgumentMatchers. argThat(req -> {
- // TODO verify the content
- return true;
- })
+ ArgumentMatchers. argThat(req -> Objects.equals(req.integrationName(), INTEGRATION_NAME) &&
+ Objects.equals(req.sourceArn(), SOURCE_ARN) &&
+ Objects.equals(req.targetArn(), TARGET_ARN) &&
+ Objects.equals(req.description(), DESCRIPTION) &&
+ Objects.equals(req.dataFilter(), DATA_FILTER))
+ );
+
+ }
+
+ @Test
+ public void handleRequest_CreateIntegration_withWeirdIntegrationAlreadyExistsException_returnFailure() {
+ // this is the variant where the service throws an IntegrationConflictOperationException instead of
+ // IntegrationAlreadyExistsException
+ when(rdsProxy.client().createIntegration(any(CreateIntegrationRequest.class)))
+ .thenThrow(IntegrationConflictOperationException.builder()
+ .message(DUPLICATE_INTEGRATION_ERROR_MESSAGE)
+ .build());
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ null,
+ () -> INTEGRATION_ACTIVE_MODEL,
+ expectFailed(HandlerErrorCode.AlreadyExists)
+ );
+
+ verify(rdsProxy.client(), times(1)).createIntegration(
+ ArgumentMatchers. argThat(req -> Objects.equals(req.integrationName(), INTEGRATION_NAME) &&
+ Objects.equals(req.sourceArn(), SOURCE_ARN) &&
+ Objects.equals(req.targetArn(), TARGET_ARN) &&
+ Objects.equals(req.description(), DESCRIPTION) &&
+ Objects.equals(req.dataFilter(), DATA_FILTER))
);
}
@@ -225,10 +254,11 @@ public void handleRequest_CreateIntegration_withDuplicateIntegrationName_returnF
);
verify(rdsProxy.client(), times(1)).createIntegration(
- ArgumentMatchers. argThat(req -> {
- // TODO verify the content
- return true;
- })
+ ArgumentMatchers. argThat(req -> Objects.equals(req.integrationName(), INTEGRATION_NAME) &&
+ Objects.equals(req.sourceArn(), SOURCE_ARN) &&
+ Objects.equals(req.targetArn(), TARGET_ARN) &&
+ Objects.equals(req.description(), DESCRIPTION) &&
+ Objects.equals(req.dataFilter(), DATA_FILTER))
);
}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/TranslatorTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/TranslatorTest.java
index fdbda7344..9db9187ef 100644
--- a/aws-rds-integration/src/test/java/software/amazon/rds/integration/TranslatorTest.java
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/TranslatorTest.java
@@ -7,6 +7,7 @@
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
import software.amazon.awssdk.services.rds.model.Filter;
+import software.amazon.awssdk.services.rds.model.ModifyIntegrationRequest;
import java.util.List;
@@ -54,4 +55,92 @@ public void translateDescribeWithoutArnOrName_shouldThrow() {
);
});
}
+
+ @Test
+ public void translateModify_differentName_shouldOnlyModifyName() {
+ ModifyIntegrationRequest request = Translator.modifyIntegrationRequest(
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name1")
+ .description("desc")
+ .dataFilter("include: d1.t1")
+ .build(),
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name2")
+ .description("desc")
+ .dataFilter("include: d1.t1")
+ .build()
+ );
+ Assertions.assertEquals(request.integrationIdentifier(), "arn");
+ Assertions.assertEquals(request.integrationName(), "name2");
+ Assertions.assertNull(request.description());
+ Assertions.assertNull(request.dataFilter());
+ }
+
+ @Test
+ public void translateModify_differentDescription_shouldOnlyModifyDescription() {
+ ModifyIntegrationRequest request = Translator.modifyIntegrationRequest(
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name1")
+ .description("desc")
+ .dataFilter("include: d1.t1")
+ .build(),
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name1")
+ .description("desc2")
+ .dataFilter("include: d1.t1")
+ .build()
+ );
+ Assertions.assertEquals(request.integrationIdentifier(), "arn");
+ Assertions.assertEquals(request.description(), "desc2");
+ Assertions.assertNull(request.integrationName());
+ Assertions.assertNull(request.dataFilter());
+ }
+
+ @Test
+ public void translateModify_differentDataFilter_shouldOnlyModifyDataFilter() {
+ ModifyIntegrationRequest request = Translator.modifyIntegrationRequest(
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name1")
+ .description("desc")
+ .dataFilter("include: d1.t1")
+ .build(),
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name1")
+ .description("desc")
+ .dataFilter("include: d1.t2")
+ .build()
+ );
+ Assertions.assertEquals(request.integrationIdentifier(), "arn");
+ Assertions.assertEquals(request.dataFilter(), "include: d1.t2");
+ Assertions.assertNull(request.integrationName());
+ Assertions.assertNull(request.description());
+ }
+
+ @Test
+ public void translateModify_differentFields_shouldModifyAll() {
+ ModifyIntegrationRequest request = Translator.modifyIntegrationRequest(
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name1")
+ .description("desc1")
+ .dataFilter("include: d1.t1")
+ .build(),
+ ResourceModel.builder()
+ .integrationArn("arn")
+ .integrationName("name2")
+ .description("desc2")
+ .dataFilter("include: d2.t2")
+ .build()
+ );
+ Assertions.assertEquals(request.integrationIdentifier(), "arn");
+ Assertions.assertEquals(request.integrationName(), "name2");
+ Assertions.assertEquals(request.description(), "desc2");
+ Assertions.assertEquals(request.dataFilter(), "include: d2.t2");
+ }
}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/UpdateHandlerTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/UpdateHandlerTest.java
index 43efd3dae..ab19562c2 100644
--- a/aws-rds-integration/src/test/java/software/amazon/rds/integration/UpdateHandlerTest.java
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/UpdateHandlerTest.java
@@ -5,6 +5,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.services.rds.RdsClient;
@@ -12,14 +13,21 @@
import software.amazon.awssdk.services.rds.model.AddTagsToResourceResponse;
import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
import software.amazon.awssdk.services.rds.model.Integration;
+import software.amazon.awssdk.services.rds.model.IntegrationAlreadyExistsException;
+import software.amazon.awssdk.services.rds.model.IntegrationConflictOperationException;
+import software.amazon.awssdk.services.rds.model.InvalidIntegrationStateException;
+import software.amazon.awssdk.services.rds.model.ModifyIntegrationRequest;
+import software.amazon.awssdk.services.rds.model.ModifyIntegrationResponse;
import software.amazon.awssdk.services.rds.model.RemoveTagsFromResourceRequest;
import software.amazon.awssdk.services.rds.model.RemoveTagsFromResourceResponse;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
import software.amazon.rds.test.common.core.HandlerName;
import java.time.Duration;
+import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -31,12 +39,21 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import static software.amazon.rds.integration.BaseHandlerStd.CALLBACK_DELAY;
@ExtendWith(MockitoExtension.class)
public class UpdateHandlerTest extends AbstractHandlerTest {
private static final String RESOURCE_UPDATED_AT = "resource-updated-at";
+ private static final String RETRIABLE_CONFLICT_OPERATION_MESSAGE = "Cannot modify because " +
+ "another operation is in progress for the " +
+ "Amazon Redshift data warehouse specified by the Amazon Resource Name (ARN). " +
+ "Try again after the current operation completes.";
+
+ private static final String RETRIABLE_INVALID_STATE_MESSAGE = "Unable to modify" +
+ " because it is not in a valid state. Wait until the integration is in a valid state and try again.";
+
@Mock
RdsClient rdsClient;
@@ -82,6 +99,8 @@ void handleRequest_Success() {
.thenReturn(RemoveTagsFromResourceResponse.builder().build());
when(rdsProxy.client().addTagsToResource(any(AddTagsToResourceRequest.class)))
.thenReturn(AddTagsToResourceResponse.builder().build());
+ when(rdsProxy.client().modifyIntegration(any(ModifyIntegrationRequest.class)))
+ .thenReturn(ModifyIntegrationResponse.builder().build());
Queue transitions = new ConcurrentLinkedQueue<>();
transitions.add(INTEGRATION_ACTIVE);
@@ -89,23 +108,255 @@ void handleRequest_Success() {
test_handleRequest_base(
new CallbackContext(),
ResourceHandlerRequest.builder()
+ .previousResourceState(Translator.translateToModel(INTEGRATION_ACTIVE))
.previousResourceTags(Translator.translateTagsToRequest(TAG_LIST))
+ .desiredResourceState(Translator.translateToModel(INTEGRATION_ACTIVE.toBuilder()
+ .description(DESCRIPTION_ALTER)
+ .dataFilter(DATA_FILTER_ALTER)
+ .integrationName(INTEGRATION_NAME_ALTER)
+ .build()))
.desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST_ALTER)),
() -> Optional.ofNullable(transitions.poll())
.orElse(INTEGRATION_ACTIVE
.toBuilder()
.tags(toAPITags(TAG_LIST_ALTER))
+ .description(DESCRIPTION_ALTER)
+ .dataFilter(DATA_FILTER_ALTER)
+ .integrationName(INTEGRATION_NAME_ALTER)
.build()),
() -> INTEGRATION_ACTIVE_MODEL,
() -> INTEGRATION_ACTIVE_MODEL.toBuilder()
.tags(TAG_LIST_ALTER)
+ .description(DESCRIPTION_ALTER)
+ .dataFilter(DATA_FILTER_ALTER)
+ .integrationName(INTEGRATION_NAME_ALTER)
.build(),
expectSuccess()
);
- verify(rdsProxy.client(), times(2)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ verify(rdsProxy.client(), times(3)).describeIntegrations(any(DescribeIntegrationsRequest.class));
verify(rdsProxy.client(), times(1)).removeTagsFromResource(any(RemoveTagsFromResourceRequest.class));
verify(rdsProxy.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class));
+ verify(rdsProxy.client(), times(1)).modifyIntegration(any(ModifyIntegrationRequest.class));
+ }
+
+ @Test
+ void handleRequest_ifBusyWithInvalidIntegrationStateException_shouldRetry() {
+ when(rdsProxy.client().modifyIntegration(any(ModifyIntegrationRequest.class)))
+ .thenThrow(InvalidIntegrationStateException.builder().message(RETRIABLE_INVALID_STATE_MESSAGE).build());
+
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ // resource handler builder
+ ResourceHandlerRequest.builder(),
+ // resource supplier - nobody calls describe
+ null,
+ // previous state
+ () -> INTEGRATION_ACTIVE_MODEL,
+ // desired state
+ () -> INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .dataFilter(DATA_FILTER_ALTER)
+ .build(),
+ // expect
+ expectInProgress(CALLBACK_DELAY)
+ );
+
+ verify(rdsProxy.client(), times(1))
+ .modifyIntegration(ArgumentMatchers.argThat((req) ->
+ Objects.equals(req.dataFilter(), DATA_FILTER_ALTER) &&
+ Objects.equals(req.integrationIdentifier(), INTEGRATION_ARN) &&
+ Objects.isNull(req.description()) &&
+ Objects.isNull(req.integrationName()))
+ );
+ }
+
+ @Test
+ void handleRequest_ifBusyWithIntegrationConflictOperationException_shouldRetry() {
+ when(rdsProxy.client().modifyIntegration(any(ModifyIntegrationRequest.class)))
+ .thenThrow(IntegrationConflictOperationException.builder().message(RETRIABLE_CONFLICT_OPERATION_MESSAGE).build());
+
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ // resource handler builder
+ ResourceHandlerRequest.builder(),
+ // resource supplier - nobody calls describe
+ null,
+ // previous state
+ () -> INTEGRATION_ACTIVE_MODEL,
+ // desired state
+ () -> INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .dataFilter(DATA_FILTER_ALTER)
+ .build(),
+ // expect
+ expectInProgress(CALLBACK_DELAY)
+ );
+
+ verify(rdsProxy.client(), times(1))
+ .modifyIntegration(ArgumentMatchers.argThat((req) ->
+ Objects.equals(req.dataFilter(), DATA_FILTER_ALTER) &&
+ Objects.equals(req.integrationIdentifier(), INTEGRATION_ARN) &&
+ Objects.isNull(req.description()) &&
+ Objects.isNull(req.integrationName()))
+ );
+ }
+
+ @Test
+ void handleRequest_partial_modify_only_description_should_only_modify_description() {
+ when(rdsProxy.client().modifyIntegration(any(ModifyIntegrationRequest.class)))
+ .thenReturn(ModifyIntegrationResponse.builder().build());
+
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ // resource handler builder
+ ResourceHandlerRequest.builder(),
+ // resource supplier
+ () -> Optional.ofNullable(transitions.poll())
+ .orElse(INTEGRATION_ACTIVE
+ .toBuilder()
+ .description(DESCRIPTION_ALTER)
+ .build()),
+ // previous state
+ () -> INTEGRATION_ACTIVE_MODEL,
+ // desired state
+ () -> INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .description(DESCRIPTION_ALTER)
+ .build(),
+ // expect
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(2)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ verify(rdsProxy.client(), times(1))
+ .modifyIntegration(ArgumentMatchers.argThat((req) ->
+ Objects.equals(req.description(), DESCRIPTION_ALTER) &&
+ Objects.equals(req.integrationIdentifier(), INTEGRATION_ARN) &&
+ Objects.isNull(req.dataFilter()) &&
+ Objects.isNull(req.integrationName()))
+ );
+ }
+
+ @Test
+ void handleRequest_partial_modify_duplicateName_shouldFailWithConflict() {
+ when(rdsProxy.client().modifyIntegration(any(ModifyIntegrationRequest.class)))
+ .thenThrow(IntegrationAlreadyExistsException.builder().message("duplicate name").build());
+
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ // resource handler builder
+ ResourceHandlerRequest.builder(),
+ // resource supplier - nobody calls describe
+ null,
+ // previous state
+ () -> INTEGRATION_ACTIVE_MODEL,
+ // desired state
+ () -> INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .integrationName(INTEGRATION_NAME_ALTER)
+ .build(),
+ // expect
+ expectFailed(HandlerErrorCode.AlreadyExists)
+ );
+
+ verify(rdsProxy.client(), times(1))
+ .modifyIntegration(ArgumentMatchers.argThat((req) ->
+ Objects.equals(req.integrationName(), INTEGRATION_NAME_ALTER) &&
+ Objects.equals(req.integrationIdentifier(), INTEGRATION_ARN) &&
+ Objects.isNull(req.dataFilter()) &&
+ Objects.isNull(req.description()))
+ );
+ }
+
+ @Test
+ void handleRequest_partial_modify_withDescriptionGoingToEmpty_shouldNotModify() {
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ // resource handler builder
+ ResourceHandlerRequest.builder(),
+ // resource supplier
+ () -> Optional.ofNullable(transitions.poll())
+ .orElse(INTEGRATION_ACTIVE),
+ // previous state
+ () -> INTEGRATION_ACTIVE_MODEL,
+ // desired state
+ () -> INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .description("")
+ .build(),
+ // expect
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(1)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ }
+
+ @Test
+ void handleRequest_partial_modify_withIntegrationNameGoingToEmpty_shouldNotGenerateANewName() {
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ // resource handler builder
+ ResourceHandlerRequest.builder(),
+ // resource supplier
+ () -> Optional.ofNullable(transitions.poll())
+ .orElse(INTEGRATION_ACTIVE),
+ // previous state
+ () -> INTEGRATION_ACTIVE_MODEL,
+ // desired state
+ () -> INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .integrationName(null)
+ .build(),
+ // expect
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(1)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ }
+
+ @Test
+ void handleRequest_partial_modify_withNoIntegrationName_shouldNotChangeIntegrationName() {
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ // resource handler builder
+ ResourceHandlerRequest.builder(),
+ // resource supplier
+ () -> Optional.ofNullable(transitions.poll())
+ .orElse(INTEGRATION_ACTIVE),
+ // previous state
+ () -> INTEGRATION_MODEL_WITH_NO_NAME,
+ // desired state
+ () -> INTEGRATION_MODEL_WITH_NO_NAME.toBuilder()
+ .description("differentdescription") // only description changed
+ .build(),
+ // expect
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(2)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ verify(rdsProxy.client(), times(1))
+ .modifyIntegration(ArgumentMatchers.argThat((req) ->
+ Objects.equals("differentdescription", req.description()) &&
+ Objects.equals(req.integrationIdentifier(), INTEGRATION_ARN) &&
+ Objects.isNull(req.dataFilter()) &&
+ Objects.isNull(req.integrationName()))
+ );
}
}
diff --git a/aws-rds-optiongroup/pom.xml b/aws-rds-optiongroup/pom.xml
index 0e5d50834..fc3a5b118 100644
--- a/aws-rds-optiongroup/pom.xml
+++ b/aws-rds-optiongroup/pom.xml
@@ -23,7 +23,7 @@
software.amazon.awssdk
rds
- 2.24.13
+ 2.25.12