diff --git a/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/ServiceProvider.java b/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/ServiceProvider.java
index 647b8b1d1..b45a8bc2a 100644
--- a/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/ServiceProvider.java
+++ b/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/ServiceProvider.java
@@ -12,7 +12,8 @@ public enum ServiceProvider {
SDK("sdk"),
S3("s3"),
MEDIAIMPORT("mediaimport"),
- ASM("secretsmanager");
+ ASM("secretsmanager"),
+ REDSHIFT("redshift");
private final String name;
diff --git a/aws-rds-integration/.gitignore b/aws-rds-integration/.gitignore
new file mode 100644
index 000000000..472ee2107
--- /dev/null
+++ b/aws-rds-integration/.gitignore
@@ -0,0 +1,28 @@
+# macOS
+.DS_Store
+._*
+
+# Maven outputs
+.classpath
+/aws-rds-integration.zip
+
+# IntelliJ
+*.iml
+.idea
+out.java
+out/
+.settings
+.project
+
+# auto-generated files
+target/
+/build/
+
+# our logs
+rpdk.log
+
+# contains credentials
+sam-tests/
+
+# auto-generated sam file
+.aws-sam/build.toml
diff --git a/aws-rds-integration/.rpdk-config b/aws-rds-integration/.rpdk-config
new file mode 100644
index 000000000..b1059adb2
--- /dev/null
+++ b/aws-rds-integration/.rpdk-config
@@ -0,0 +1,20 @@
+{
+ "artifact_type": "RESOURCE",
+ "typeName": "AWS::RDS::Integration",
+ "language": "java",
+ "runtime": "java8",
+ "entrypoint": "software.amazon.rds.integration.HandlerWrapper::handleRequest",
+ "testEntrypoint": "software.amazon.rds.integration.HandlerWrapper::testEntrypoint",
+ "settings": {
+ "namespace": [
+ "software",
+ "amazon",
+ "rds",
+ "integration"
+ ],
+ "codegen_template_path": "guided_aws",
+ "protocolVersion": "2.0.0"
+ },
+ "logProcessorEnabled": "true",
+ "executableEntrypoint": "software.amazon.rds.integration.HandlerWrapperExecutable"
+}
diff --git a/aws-rds-integration/README.md b/aws-rds-integration/README.md
new file mode 100644
index 000000000..f3c3a6433
--- /dev/null
+++ b/aws-rds-integration/README.md
@@ -0,0 +1,28 @@
+## aws-cloudformation-resource-providers-rds
+
+The CloudFormation Resource Provider Package For Amazon Relational Database Service
+
+## 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
diff --git a/aws-rds-integration/aws-rds-integration.json b/aws-rds-integration/aws-rds-integration.json
new file mode 100644
index 000000000..d66806896
--- /dev/null
+++ b/aws-rds-integration/aws-rds-integration.json
@@ -0,0 +1,155 @@
+{
+ "typeName": "AWS::RDS::Integration",
+ "description": "An example resource schema demonstrating some basic constructs and validation rules.",
+ "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git",
+ "properties": {
+ "IntegrationName": {
+ "description": "The name of the integration.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ },
+ "Tags": {
+ "type": "array",
+ "maxItems": 50,
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "description": "An array of key-value pairs to apply to this resource.",
+ "items": {
+ "$ref": "#/definitions/Tag"
+ }
+ },
+ "SourceArn": {
+ "type": "string",
+ "description": "The Amazon Resource Name (ARN) of the Aurora DB cluster to use as the source for replication."
+ },
+ "TargetArn": {
+ "type": "string",
+ "description": "The ARN of the Redshift data warehouse to use as the target for replication."
+ },
+ "IntegrationArn": {
+ "type": "string",
+ "description": "The ARN of the integration."
+ },
+ "KMSKeyId": {
+ "type": "string",
+ "description": "An optional AWS Key Management System (AWS KMS) key ARN for the key used to to encrypt the integration. The resource accepts the key ID and the key ARN forms. The key ID form can be used if the KMS key is owned by te same account. If the KMS key belongs to a different account than the calling account, the full key ARN must be specified. Do not use the key alias or the key alias ARN as this will cause a false drift of the resource."
+ },
+ "AdditionalEncryptionContext": {
+ "$ref": "#/definitions/EncryptionContextMap"
+ },
+ "CreateTime": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "SourceArn",
+ "TargetArn"
+ ],
+ "definitions": {
+ "Tags": {
+ "type": "array",
+ "maxItems": 50,
+ "uniqueItems": true,
+ "insertionOrder": false,
+ "description": "An array of key-value pairs to apply to this resource.",
+ "items": {
+ "$ref": "#/definitions/Tag"
+ }
+ },
+ "Tag": {
+ "description": "A key-value pair to associate with a resource.",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "Key": {
+ "type": "string",
+ "description": "The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ",
+ "minLength": 1,
+ "maxLength": 128
+ },
+ "Value": {
+ "type": "string",
+ "description": "The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ",
+ "minLength": 0,
+ "maxLength": 256
+ }
+ },
+ "required": [
+ "Key"
+ ]
+ },
+ "EncryptionContextMap": {
+ "type": "object",
+ "patternProperties": {
+ "^[\\s\\S]*$": {
+ "type": "string",
+ "maxLength": 131072,
+ "minLength": 0
+ }
+ },
+ "description": "An optional set of non-secret key\u2013value pairs that contains additional contextual information about the data.",
+ "additionalProperties": false
+ }
+ },
+ "propertyTransform": {
+ "/properties/SourceArn": "$lowercase(SourceArn)",
+ "/properties/KmsKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", KmsKeyId])"
+ },
+ "createOnlyProperties": [
+ "/properties/SourceArn",
+ "/properties/TargetArn",
+ "/properties/KMSKeyId",
+ "/properties/AdditionalEncryptionContext",
+ "/properties/IntegrationName"
+ ],
+ "readOnlyProperties": [
+ "/properties/IntegrationArn",
+ "/properties/CreateTime"
+ ],
+ "primaryIdentifier": [
+ "/properties/IntegrationArn"
+ ],
+ "handlers": {
+ "create": {
+ "permissions": [
+ "rds:CreateIntegration",
+ "rds:DescribeIntegrations",
+ "rds:AddTagsToResource",
+ "kms:CreateGrant",
+ "kms:DescribeKey",
+ "redshift:CreateInboundIntegration"
+ ]
+ },
+ "read": {
+ "permissions": [
+ "rds:DescribeIntegrations"
+ ]
+ },
+ "update": {
+ "permissions": [
+ "rds:DescribeIntegrations",
+ "rds:AddTagsToResource",
+ "rds:RemoveTagsFromResource"
+ ]
+ },
+ "delete": {
+ "permissions": [
+ "rds:DeleteIntegration",
+ "rds:DescribeIntegrations"
+ ]
+ },
+ "list": {
+ "permissions": [
+ "rds:DescribeIntegrations"
+ ]
+ }
+ },
+ "tagging": {
+ "taggable": true,
+ "tagOnCreate": true,
+ "tagUpdatable": true,
+ "tagProperty": "/properties/Tags"
+ },
+ "additionalProperties": false
+}
diff --git a/aws-rds-integration/docs/README.md b/aws-rds-integration/docs/README.md
new file mode 100644
index 000000000..78c9928dd
--- /dev/null
+++ b/aws-rds-integration/docs/README.md
@@ -0,0 +1,123 @@
+# AWS::RDS::Integration
+
+An example resource schema demonstrating some basic constructs and validation rules.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Type" : "AWS::RDS::Integration",
+ "Properties" : {
+ "IntegrationName " : String ,
+ "Tags " : [ Tag , ... ] ,
+ "SourceArn " : String ,
+ "TargetArn " : String ,
+ "KMSKeyId " : String ,
+ "AdditionalEncryptionContext " : AdditionalEncryptionContext ,
+ }
+}
+
+
+### YAML
+
+
+Type: AWS::RDS::Integration
+Properties:
+ IntegrationName : String
+ Tags :
+ - Tag
+ SourceArn : String
+ TargetArn : String
+ KMSKeyId : String
+ AdditionalEncryptionContext : AdditionalEncryptionContext
+
+
+## Properties
+
+#### IntegrationName
+
+The name of the integration.
+
+_Required_: No
+
+_Type_: String
+
+_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)
+
+#### Tags
+
+An array of key-value pairs to apply to this resource.
+
+_Required_: No
+
+_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)
+
+#### SourceArn
+
+The Amazon Resource Name (ARN) of the Aurora DB cluster to use as the source for replication.
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### TargetArn
+
+The ARN of the Redshift data warehouse to use as the target for replication.
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### KMSKeyId
+
+An optional AWS Key Management System (AWS KMS) key ARN for the key used to to encrypt the integration. The resource accepts the key ID and the key ARN forms. The key ID form can be used if the KMS key is owned by te same account. If the KMS key belongs to a different account than the calling account, the full key ARN must be specified. Do not use the key alias or the key alias ARN as this will cause a false drift of the resource.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### AdditionalEncryptionContext
+
+An optional set of non-secret key–value pairs that contains additional contextual information about the data.
+
+_Required_: No
+
+_Type_: AdditionalEncryptionContext
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Ref
+
+When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the IntegrationArn.
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### IntegrationArn
+
+The ARN of the integration.
+
+#### CreateTime
+
+Returns the CreateTime
value.
diff --git a/aws-rds-integration/docs/additionalencryptioncontext.md b/aws-rds-integration/docs/additionalencryptioncontext.md
new file mode 100644
index 000000000..c0509f689
--- /dev/null
+++ b/aws-rds-integration/docs/additionalencryptioncontext.md
@@ -0,0 +1,33 @@
+# AWS::RDS::Integration AdditionalEncryptionContext
+
+An optional set of non-secret key–value pairs that contains additional contextual information about the data.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "^[\s\S]*$ " : String
+}
+
+
+### YAML
+
+
+^[\s\S]*$ : String
+
+
+## Properties
+
+#### \^[\s\S]*$
+
+_Required_: No
+
+_Type_: String
+
+_Maximum Length_: 131072
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
diff --git a/aws-rds-integration/docs/tag.md b/aws-rds-integration/docs/tag.md
new file mode 100644
index 000000000..da7ae8c25
--- /dev/null
+++ b/aws-rds-integration/docs/tag.md
@@ -0,0 +1,51 @@
+# AWS::RDS::Integration Tag
+
+A key-value pair to associate with a resource.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Key " : String ,
+ "Value " : String
+}
+
+
+### YAML
+
+
+Key : String
+Value : String
+
+
+## Properties
+
+#### Key
+
+The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum Length_: 1
+
+_Maximum Length_: 128
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Value
+
+The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum Length_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
diff --git a/aws-rds-integration/lombok.config b/aws-rds-integration/lombok.config
new file mode 100644
index 000000000..7a21e8804
--- /dev/null
+++ b/aws-rds-integration/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
diff --git a/aws-rds-integration/pom.xml b/aws-rds-integration/pom.xml
new file mode 100644
index 000000000..2f97cde52
--- /dev/null
+++ b/aws-rds-integration/pom.xml
@@ -0,0 +1,221 @@
+
+
+ 4.0.0
+
+ software.amazon.rds.integration
+ aws-rds-integration-handler
+ aws-rds-integration-handler
+ 1.0-SNAPSHOT
+ jar
+
+
+ 1.8
+ 1.8
+ UTF-8
+ UTF-8
+
+
+
+
+ software.amazon.awssdk
+ rds
+ 2.21.17
+
+
+
+ software.amazon.cloudformation
+ aws-cloudformation-rpdk-java-plugin
+ [2.0.0,3.0.0)
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.22
+ provided
+
+
+
+
+ org.assertj
+ assertj-core
+ 3.22.0
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.8.2
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 4.3.1
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 4.3.1
+ test
+
+
+ software.amazon.rds.common
+ aws-rds-cfn-common
+ 1.0
+ compile
+
+
+ software.amazon.rds.common
+ aws-rds-cfn-test-common
+ 1.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ -Xlint:all,-options,-processing
+ -Werror
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.3
+
+ false
+
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+
+ generate
+ generate-sources
+
+ exec
+
+
+ cfn
+ generate
+ ${project.basedir}
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ${project.basedir}/target/generated-sources/rpdk
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 2.4
+
+
+ maven-surefire-plugin
+ 3.0.0-M3
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.4
+
+
+ **/BaseConfiguration*
+ **/BaseHandler*
+ **/HandlerWrapper*
+ **/ResourceModel*
+
+
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+ jacoco-check
+
+ check
+
+
+
+
+ PACKAGE
+
+
+ BRANCH
+ COVEREDRATIO
+ 0.8
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.8
+
+
+
+
+
+
+
+
+
+
+
+ ${project.basedir}
+
+ aws-rds-integration.json
+
+
+
+
+
diff --git a/aws-rds-integration/resource-role.yaml b/aws-rds-integration/resource-role.yaml
new file mode 100644
index 000000000..5caac3852
--- /dev/null
+++ b/aws-rds-integration/resource-role.yaml
@@ -0,0 +1,45 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Description: >
+ This CloudFormation template creates a role assumed by CloudFormation
+ during CRUDL operations to mutate resources on behalf of the customer.
+
+Resources:
+ ExecutionRole:
+ Type: AWS::IAM::Role
+ Properties:
+ MaxSessionDuration: 8400
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: resources.cloudformation.amazonaws.com
+ Action: sts:AssumeRole
+ Condition:
+ StringEquals:
+ aws:SourceAccount:
+ Ref: AWS::AccountId
+ StringLike:
+ aws:SourceArn:
+ Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/AWS-RDS-Integration/*
+ Path: "/"
+ Policies:
+ - PolicyName: ResourceTypePolicy
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - "kms:CreateGrant"
+ - "kms:DescribeKey"
+ - "rds:AddTagsToResource"
+ - "rds:CreateIntegration"
+ - "rds:DeleteIntegration"
+ - "rds:DescribeIntegrations"
+ - "rds:RemoveTagsFromResource"
+ - "redshift:CreateInboundIntegration"
+ Resource: "*"
+Outputs:
+ ExecutionRoleArn:
+ Value:
+ Fn::GetAtt: ExecutionRole.Arn
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
new file mode 100644
index 000000000..87a310aaa
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java
@@ -0,0 +1,182 @@
+package software.amazon.rds.integration;
+
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.IntegrationAlreadyExistsException;
+import software.amazon.awssdk.services.rds.model.IntegrationConflictOperationException;
+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.KmsKeyNotAccessibleException;
+import software.amazon.awssdk.services.rds.model.Tag;
+import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.Logger;
+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;
+import software.amazon.rds.common.logging.LoggingProxyClient;
+import software.amazon.rds.common.logging.RequestLogger;
+import software.amazon.rds.common.printer.FilteredJsonPrinter;
+
+import java.util.Collection;
+
+public abstract class BaseHandlerStd extends BaseHandler {
+ protected static final String STACK_NAME = "rds";
+ protected static final String RESOURCE_IDENTIFIER = "integration";
+ protected static final int MAX_LENGTH_INTEGRATION = 64;
+
+ protected static final ErrorRuleSet DEFAULT_INTEGRATION_ERROR_RULE_SET = ErrorRuleSet
+ .extend(Commons.DEFAULT_ERROR_RULE_SET)
+ .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.AlreadyExists),
+ IntegrationAlreadyExistsException.class)
+ .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.NotFound),
+ IntegrationNotFoundException.class)
+ .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ResourceConflict),
+ IntegrationConflictOperationException.class)
+ .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ServiceLimitExceeded),
+ IntegrationQuotaExceededException.class)
+ .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.AccessDenied),
+ KmsKeyNotAccessibleException.class)
+ .build();
+
+ private final FilteredJsonPrinter PARAMETERS_FILTER = new FilteredJsonPrinter();
+ private final IntegrationStatusUtil integrationStatusUtil;
+
+ /** Custom handler config, mostly to facilitate faster unit test */
+ final HandlerConfig config;
+
+ public BaseHandlerStd() {
+ this(HandlerConfig.builder().build());
+ }
+
+ BaseHandlerStd(HandlerConfig config) {
+ this.config = config;
+ this.integrationStatusUtil = new IntegrationStatusUtil();
+ }
+
+ @Override
+ public final ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger) {
+ return RequestLogger.handleRequest(
+ logger,
+ request,
+ PARAMETERS_FILTER,
+ requestLogger -> handleRequest(
+ proxy,
+ request,
+ callbackContext != null ? callbackContext : new CallbackContext(),
+ new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)),
+ logger
+ ));
+ }
+
+ protected abstract ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger);
+
+
+ /**
+ * Integration is stablized when it's in active state.
+ * @param model
+ * @param proxyClient
+ * @return
+ */
+ protected boolean isStabilized(final ResourceModel model, final ProxyClient proxyClient) {
+ final IntegrationStatus status = proxyClient.injectCredentialsAndInvokeV2(
+ Translator.describeIntegrationsRequest(model),
+ proxyClient.client()::describeIntegrations)
+ .integrations().stream().findFirst().get().status();
+
+ assertIntegrationInValidCreatingState(status);
+ return integrationStatusUtil.isStabilizedState(status);
+ }
+
+ /**
+ * Assert that the integration is in a valid state that can continue creating.
+ *
+ * @throws CfnNotStabilizedException if the integration is in a state that cannot continue creating.
+ * @param integrationStatus
+ */
+ void assertIntegrationInValidCreatingState(IntegrationStatus integrationStatus) {
+ if (!integrationStatusUtil.isValidCreatingStatus(integrationStatus)) {
+ throw new CfnNotStabilizedException(
+ new Exception("Integration is in state a state that cannot complete creation: " + integrationStatus));
+ }
+ }
+
+
+ protected ProgressEvent updateTags(
+ final AmazonWebServicesClientProxy proxy,
+ final ProxyClient rdsProxyClient,
+ final ProgressEvent progress,
+ final Tagging.TagSet previousTags,
+ final Tagging.TagSet desiredTags) {
+ final Collection effectivePreviousTags = Tagging.translateTagsToSdk(previousTags);
+ final Collection effectiveDesiredTags = Tagging.translateTagsToSdk(desiredTags);
+
+ final Collection tagsToRemove = Tagging.exclude(effectivePreviousTags, effectiveDesiredTags);
+ final Collection tagsToAdd = Tagging.exclude(effectiveDesiredTags, effectivePreviousTags);
+
+ if (tagsToAdd.isEmpty() && tagsToRemove.isEmpty()) {
+ return progress;
+ }
+
+ // TODO - we should call "add" on updated tags, not "remove and then add".
+ final Tagging.TagSet rulesetTagsToAdd = Tagging.exclude(desiredTags, previousTags);
+ final Tagging.TagSet rulesetTagsToRemove = Tagging.exclude(previousTags, desiredTags);
+
+ String arn = progress.getCallbackContext().getIntegrationArn();
+ if (arn == null) {
+ ProgressEvent progressEvent = fetchIntegrationArn(proxy, rdsProxyClient, progress);
+ if (progressEvent.isFailed()) {
+ return progressEvent;
+ }
+ arn = progressEvent.getCallbackContext().getIntegrationArn();
+ }
+
+ try {
+ Tagging.removeTags(rdsProxyClient, arn, Tagging.translateTagsToSdk(tagsToRemove));
+ Tagging.addTags(rdsProxyClient, arn, Tagging.translateTagsToSdk(tagsToAdd));
+ } catch (Exception exception) {
+ return Commons.handleException(
+ progress,
+ exception,
+ // Integration resource will NOT allow soft fail on tag updates.
+ DEFAULT_INTEGRATION_ERROR_RULE_SET
+ );
+ }
+
+ return progress;
+ }
+
+ protected ProgressEvent fetchIntegrationArn(final AmazonWebServicesClientProxy proxy,
+ final ProxyClient proxyClient,
+ final ProgressEvent progress) {
+ return proxy.initiate("rds::read-integration-arn", proxyClient, progress.getResourceModel(), progress.getCallbackContext())
+ .translateToServiceRequest(Translator::describeIntegrationsRequest)
+ .makeServiceCall((describeIntegrationsRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(describeIntegrationsRequest, proxyInvocation.client()::describeIntegrations))
+ .handleError((describeIntegrationsRequest, exception, client, resourceModel, ctx) ->
+ Commons.handleException(
+ ProgressEvent.progress(resourceModel, ctx),
+ exception,
+ DEFAULT_INTEGRATION_ERROR_RULE_SET
+ ))
+ .done((describeIntegrationsRequest, describeIntegrationsResponse, proxyInvocation, resourceModel, context) -> {
+ final String arn = describeIntegrationsResponse.integrations().stream().findFirst().get().integrationArn();
+ context.setIntegrationArn(arn);
+ return ProgressEvent.progress(resourceModel, context);
+ });
+ }
+}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/CallbackContext.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CallbackContext.java
new file mode 100644
index 000000000..712ef1ca6
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CallbackContext.java
@@ -0,0 +1,28 @@
+package software.amazon.rds.integration;
+
+import software.amazon.cloudformation.proxy.StdCallbackContext;
+import software.amazon.rds.common.handler.TaggingContext;
+
+@lombok.Getter
+@lombok.Setter
+@lombok.ToString
+@lombok.EqualsAndHashCode(callSuper = true)
+public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider {
+ private String integrationArn;
+
+ private TaggingContext taggingContext;
+ // used to keep track of post-delete delay in seconds
+ // used in software.amazon.rds.integration.DeleteHandler.delay
+ private int deleteWaitTime;
+
+ public CallbackContext() {
+ super();
+ this.taggingContext = new TaggingContext();
+ }
+
+ @Override
+ public TaggingContext getTaggingContext() {
+ return taggingContext;
+ }
+
+}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/ClientProvider.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/ClientProvider.java
new file mode 100644
index 000000000..fc1afa38d
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/ClientProvider.java
@@ -0,0 +1,13 @@
+package software.amazon.rds.integration;
+
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.RdsClientBuilder;
+import software.amazon.rds.common.client.BaseSdkClientProvider;
+
+public class ClientProvider extends BaseSdkClientProvider {
+
+ @Override
+ public RdsClient getClient() {
+ return setHttpClient(setUserAgent(RdsClient.builder())).build();
+ }
+}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/Configuration.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/Configuration.java
new file mode 100644
index 000000000..4f93c428f
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/Configuration.java
@@ -0,0 +1,36 @@
+package software.amazon.rds.integration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import com.amazonaws.util.CollectionUtils;
+
+class Configuration extends BaseConfiguration {
+
+ public Configuration() {
+ super("aws-rds-integration.json");
+ }
+
+ public JSONObject resourceSchemaJsonObject() {
+ return new JSONObject(new JSONTokener(this.getClass().getClassLoader().getResourceAsStream(schemaFilename)));
+ }
+
+ /**
+ * Converts the resource-defined tags into a plain Java map.
+ * @param model
+ * @return
+ */
+ public Map resourceDefinedTags(final ResourceModel model) {
+ if (CollectionUtils.isNullOrEmpty(model.getTags()))
+ return null;
+
+ final Map tagMap = new HashMap<>();
+ for (final Tag tag : model.getTags()) {
+ tagMap.put(tag.getKey(), tag.getValue());
+ }
+ return tagMap;
+ }
+}
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
new file mode 100644
index 000000000..d9d49b078
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java
@@ -0,0 +1,103 @@
+package software.amazon.rds.integration;
+
+import com.amazonaws.util.StringUtils;
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.IntegrationConflictOperationException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.Logger;
+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 software.amazon.rds.common.util.IdentifierFactory;
+
+import java.util.HashSet;
+import java.util.Optional;
+
+public class CreateHandler extends BaseHandlerStd {
+
+ private final static String INTEGRATION_NAME_CONFLICT_ERROR_MESSAGE = "Integration names must be unique within an account";
+ private final static IdentifierFactory integrationNameFactory = new IdentifierFactory(
+ STACK_NAME,
+ RESOURCE_IDENTIFIER,
+ MAX_LENGTH_INTEGRATION
+ );
+
+ /** Default constructor w/ default backoff */
+ public CreateHandler() {
+ }
+
+ /** Default constructor w/ custom config */
+ public CreateHandler(HandlerConfig config) {
+ super(config);
+ }
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ final ResourceModel model = request.getDesiredResourceState();
+
+ final Tagging.TagSet allTags = Tagging.TagSet.builder()
+ .systemTags(Tagging.translateTagsToSdk(request.getSystemTags()))
+ .stackTags(Tagging.translateTagsToSdk(request.getDesiredResourceTags()))
+ .resourceTags(new HashSet<>(Translator.translateTagsToSdk(request.getDesiredResourceState().getTags())))
+ .build();
+
+ return ProgressEvent.progress(model, callbackContext)
+ .then(progress -> setIntegrationNameIfEmpty(request, progress))
+ .then(progress -> createIntegration(proxy, proxyClient, progress, allTags))
+ .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
+ }
+
+ private ProgressEvent createIntegration(final AmazonWebServicesClientProxy proxy,
+ final ProxyClient proxyClient,
+ final ProgressEvent progress,
+ final Tagging.TagSet tags) {
+ return proxy.initiate("rds::create-integration", proxyClient, progress.getResourceModel(), progress.getCallbackContext())
+ .translateToServiceRequest((resourceModel) -> Translator.createIntegrationRequest(resourceModel, tags))
+ .backoffDelay(config.getBackoff())
+ .makeServiceCall((createIntegrationRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(createIntegrationRequest, proxyInvocation.client()::createIntegration))
+ .stabilize((createIntegrationRequest, createIntegrationResponse, proxyInvocation, resourceModel, context) -> {
+ // with the response, now we'd know what the ARN is.
+ resourceModel.setIntegrationArn(
+ Optional.ofNullable(resourceModel.getIntegrationArn()).orElse(createIntegrationResponse.integrationArn())
+ );
+ return isStabilized(resourceModel, proxyInvocation);
+ })
+ .handleError((createRequest, exception, client, resourceModel, ctx) -> {
+ // 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 (IntegrationConflictOperationException.class.isAssignableFrom(exception.getClass())) {
+ if (Optional.ofNullable(exception.getMessage()).orElse("").contains(INTEGRATION_NAME_CONFLICT_ERROR_MESSAGE)) {
+ return ProgressEvent.failed(null, null, HandlerErrorCode.AlreadyExists, exception.getMessage());
+ }
+ }
+ return Commons.handleException(
+ ProgressEvent.progress(resourceModel, ctx),
+ exception,
+ DEFAULT_INTEGRATION_ERROR_RULE_SET);
+ })
+ .progress();
+ }
+
+ private ProgressEvent setIntegrationNameIfEmpty(final ResourceHandlerRequest request,
+ final ProgressEvent progress
+ ) {
+ ResourceModel model = progress.getResourceModel();
+ if (StringUtils.isNullOrEmpty(model.getIntegrationName())) {
+ model.setIntegrationName(integrationNameFactory.newIdentifier()
+ .withStackId(request.getStackId())
+ .withResourceId(request.getLogicalResourceIdentifier())
+ .withRequestToken(request.getClientRequestToken())
+ .toString());
+ }
+ return 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
new file mode 100644
index 000000000..87b5444b2
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/DeleteHandler.java
@@ -0,0 +1,93 @@
+package software.amazon.rds.integration;
+
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.IntegrationNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+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;
+
+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.
+ static final int POST_DELETION_DELAY_SEC = 500;
+ static final int CALLBACK_DELAY = 6;
+
+ /** Default constructor w/ default backoff */
+ public DeleteHandler() {}
+
+ /** Default constructor w/ custom config */
+ public DeleteHandler(HandlerConfig config) {
+ super(config);
+ }
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+ return checkIfIntegrationExists(proxy, request, callbackContext, proxyClient)
+ .then((evt) -> proxy.initiate("rds::delete-integration", proxyClient, request.getDesiredResourceState(), callbackContext)
+ .translateToServiceRequest(Translator::deleteIntegrationRequest)
+ .backoffDelay(config.getBackoff())
+ .makeServiceCall((deleteIntegrationRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(deleteIntegrationRequest, proxyInvocation.client()::deleteIntegration))
+ .stabilize((deleteIntegrationRequest, deleteIntegrationResponse, proxyInvocation, model, context) ->
+ isDeleted(model, proxyInvocation))
+ .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
+ ErrorRuleSet.extend(DEFAULT_INTEGRATION_ERROR_RULE_SET)
+ .withErrorClasses(ErrorStatus.ignore(), IntegrationNotFoundException.class)
+ .build()
+ ))
+ .progress()
+ .then((e) -> delay(e, POST_DELETION_DELAY_SEC))
+ .then((e) -> ProgressEvent.defaultSuccessHandler(null)));
+ }
+
+ private ProgressEvent checkIfIntegrationExists(final AmazonWebServicesClientProxy proxy,
+ 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())
+ .makeServiceCall(((describeIntegrationsRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(describeIntegrationsRequest, proxyInvocation.client()::describeIntegrations)))
+ .handleError((deleteRequest, exception, client, resourceModel, ctx) -> Commons.handleException(
+ ProgressEvent.progress(resourceModel, ctx),
+ exception,
+ DEFAULT_INTEGRATION_ERROR_RULE_SET
+ ))
+ .progress();
+ }
+
+ /** Inserts an artificial delay */
+ private ProgressEvent delay(final ProgressEvent evt, final int seconds) {
+ CallbackContext callbackContext = evt.getCallbackContext();
+ if (callbackContext.getDeleteWaitTime() <= seconds) {
+ callbackContext.setDeleteWaitTime(callbackContext.getDeleteWaitTime() + CALLBACK_DELAY);
+ return ProgressEvent.defaultInProgressHandler(callbackContext, CALLBACK_DELAY, evt.getResourceModel());
+ } else {
+ return ProgressEvent.progress(evt.getResourceModel(), callbackContext);
+ }
+ }
+
+ protected boolean isDeleted(final ResourceModel model,
+ final ProxyClient proxyClient) {
+ try {
+ proxyClient.injectCredentialsAndInvokeV2(Translator.describeIntegrationsRequest(model), proxyClient.client()::describeIntegrations);
+ return false;
+ } catch (IntegrationNotFoundException e) {
+ return true;
+ }
+ }
+}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/IntegrationStatusUtil.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/IntegrationStatusUtil.java
new file mode 100644
index 000000000..2ae151934
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/IntegrationStatusUtil.java
@@ -0,0 +1,32 @@
+package software.amazon.rds.integration;
+
+import com.google.common.collect.ImmutableSet;
+import software.amazon.awssdk.services.rds.model.IntegrationStatus;
+
+import java.util.Set;
+
+public class IntegrationStatusUtil {
+
+ private final Set VALID_CREATING_STATES = ImmutableSet.of(
+ IntegrationStatus.CREATING,
+ IntegrationStatus.SYNCING,
+ IntegrationStatus.MODIFYING,
+ IntegrationStatus.NEEDS_ATTENTION,
+ IntegrationStatus.ACTIVE
+ );
+
+ private final Set STABILIZED_STATES = ImmutableSet.of(
+ IntegrationStatus.NEEDS_ATTENTION,
+ IntegrationStatus.ACTIVE
+ );
+
+
+ public boolean isValidCreatingStatus(IntegrationStatus status) {
+ return VALID_CREATING_STATES.contains(status);
+ }
+
+ public boolean isStabilizedState(IntegrationStatus status) {
+ return STABILIZED_STATES.contains(status);
+ }
+
+}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/ListHandler.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/ListHandler.java
new file mode 100644
index 000000000..398036f85
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/ListHandler.java
@@ -0,0 +1,57 @@
+package software.amazon.rds.integration;
+
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsResponse;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.OperationStatus;
+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 java.util.stream.Collectors;
+
+public class ListHandler extends BaseHandlerStd {
+
+ /** Default constructor w/ default backoff */
+ public ListHandler() {}
+
+ /** Default constructor w/ custom config */
+ public ListHandler(HandlerConfig config) {
+ super(config);
+ }
+
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+
+ DescribeIntegrationsResponse describeIntegrationsResponse;
+ try {
+ describeIntegrationsResponse = proxy.injectCredentialsAndInvokeV2(
+ Translator.describeIntegrationsRequest(request.getNextToken()),
+ proxyClient.client()::describeIntegrations);
+ } catch (Exception e) {
+ return Commons.handleException(
+ ProgressEvent.progress(request.getDesiredResourceState(), callbackContext),
+ e,
+ DEFAULT_INTEGRATION_ERROR_RULE_SET
+ );
+ }
+
+ return ProgressEvent.builder()
+ .resourceModels(
+ describeIntegrationsResponse.integrations()
+ .stream()
+ .map(Translator::translateToModel).collect(Collectors.toList())
+ ).nextToken(describeIntegrationsResponse.marker())
+ .status(OperationStatus.SUCCESS)
+ .build();
+ }
+}
diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/ReadHandler.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/ReadHandler.java
new file mode 100644
index 000000000..80e0fb0aa
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/ReadHandler.java
@@ -0,0 +1,47 @@
+package software.amazon.rds.integration;
+
+
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.Integration;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+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;
+
+public class ReadHandler extends BaseHandlerStd {
+ /** Default constructor w/ default backoff */
+ public ReadHandler() {}
+
+ /** Default constructor w/ custom config */
+ public ReadHandler(HandlerConfig config) {
+ super(config);
+ }
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger
+ ) {
+ return proxy.initiate("rds::describe-integration", proxyClient, request.getDesiredResourceState(), callbackContext)
+ .translateToServiceRequest(Translator::describeIntegrationsRequest)
+ .backoffDelay(config.getBackoff())
+ .makeServiceCall((describeIntegrationsRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(describeIntegrationsRequest, proxyInvocation.client()::describeIntegrations))
+ .handleError((describeRequest, exception, client, resourceModel, ctx) -> Commons.handleException(
+ ProgressEvent.progress(resourceModel, ctx),
+ exception,
+ DEFAULT_INTEGRATION_ERROR_RULE_SET))
+ .done((describeIntegrationsRequest, describeIntegrationsResponse, proxyInvocation, model, context) -> {
+ final Integration integration = describeIntegrationsResponse.integrations().stream().findFirst().get();
+ // it's possible the model does not have the ARN populated yet,
+ // so we can just be conservative and populate it at all times.
+ model.setIntegrationArn(integration.integrationArn());
+ return ProgressEvent.success(Translator.translateToModel(integration), context);
+ });
+ }
+
+}
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
new file mode 100644
index 000000000..8edc6583a
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/Translator.java
@@ -0,0 +1,114 @@
+package software.amazon.rds.integration;
+
+import com.amazonaws.util.CollectionUtils;
+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.rds.common.handler.Tagging;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.stream.Collectors;
+
+public class Translator {
+ private static final TimeZone TZ_UTC = TimeZone.getTimeZone("UTC");
+ private static final DateFormat DATETIME_FORMATTER;
+ static {
+ // this is mimicking what the AWS CLI gives you.
+ DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX");
+ DATETIME_FORMATTER.setTimeZone(TZ_UTC);
+ }
+
+ public static CreateIntegrationRequest createIntegrationRequest(
+ final ResourceModel model,
+ final Tagging.TagSet tags
+ ) {
+ return CreateIntegrationRequest.builder()
+ .kmsKeyId(model.getKMSKeyId())
+ .integrationName(model.getIntegrationName())
+ .sourceArn(model.getSourceArn())
+ .targetArn(model.getTargetArn())
+ .additionalEncryptionContext(model.getAdditionalEncryptionContext())
+ .tags(Tagging.translateTagsToSdk(tags))
+ .build();
+ }
+
+ static DescribeIntegrationsRequest describeIntegrationsRequest(final ResourceModel model) {
+ DescribeIntegrationsRequest.Builder describeRequestBuilder = DescribeIntegrationsRequest.builder();
+ if (model.getIntegrationArn() != null) {
+ describeRequestBuilder.integrationIdentifier(model.getIntegrationArn());
+ } else if (model.getIntegrationName() != null){
+ describeRequestBuilder.filters(Filter.builder().name(model.getIntegrationName()).build());
+ } else {
+ throw new RuntimeException("The integration model has neither the ARN nor the name: " + model);
+ }
+ return describeRequestBuilder.build();
+ }
+
+ static DescribeIntegrationsRequest describeIntegrationsRequest(final String nextToken) {
+ return DescribeIntegrationsRequest.builder()
+ .marker(nextToken)
+ .build();
+ }
+
+ static DeleteIntegrationRequest deleteIntegrationRequest(final ResourceModel model) {
+ return DeleteIntegrationRequest.builder()
+ .integrationIdentifier(model.getIntegrationArn())
+ .build();
+ }
+ public static List translateTagsToSdk(final Collection tags) {
+ return Optional.ofNullable(tags).orElse(Collections.emptyList())
+ .stream()
+ .map(tag -> software.amazon.awssdk.services.rds.model.Tag.builder()
+ .key(tag.getKey())
+ .value(tag.getValue())
+ .build()
+ )
+ .collect(Collectors.toList());
+ }
+
+ static Set translateTags(final Collection rdsTags) {
+ return CollectionUtils.isNullOrEmpty(rdsTags) ? null
+ : rdsTags.stream()
+ .map(tag -> Tag.builder()
+ .key(tag.key())
+ .value(tag.value())
+ .build())
+ .collect(Collectors.toSet());
+ }
+
+ public static Map translateTagsToRequest(final Collection tags) {
+ return Optional.ofNullable(tags).orElse(Collections.emptyList())
+ .stream()
+ .collect(Collectors.toMap(Tag::getKey, Tag::getValue));
+ }
+
+ static ResourceModel translateToModel(
+ final Integration integration
+ ) {
+ return ResourceModel.builder()
+ .createTime(
+ Optional.ofNullable(integration.createTime())
+ .map(Date::from)
+ .map(DATETIME_FORMATTER::format)
+ .orElse(null))
+ .sourceArn(integration.sourceArn())
+ .integrationArn(integration.integrationArn())
+ .integrationName(integration.integrationName())
+ .targetArn(integration.targetArn())
+ .kMSKeyId(integration.kmsKeyId())
+ .tags(translateTags(integration.tags()))
+ .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
new file mode 100644
index 000000000..dee8f11f1
--- /dev/null
+++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/UpdateHandler.java
@@ -0,0 +1,50 @@
+package software.amazon.rds.integration;
+
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import software.amazon.rds.common.handler.HandlerConfig;
+import software.amazon.rds.common.handler.Tagging;
+
+import java.util.HashSet;
+
+public class UpdateHandler extends BaseHandlerStd {
+ /** Default constructor w/ default backoff */
+ public UpdateHandler() {}
+
+ /** Default constructor w/ custom config */
+ public UpdateHandler(HandlerConfig config) {
+ super(config);
+ }
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger
+ ) {
+ // Currently Integration resource only supports Tags update.
+ final ResourceModel desiredModel = request.getDesiredResourceState();
+
+ final Tagging.TagSet previousTags = Tagging.TagSet.builder()
+ .systemTags(Tagging.translateTagsToSdk(request.getPreviousSystemTags()))
+ .stackTags(Tagging.translateTagsToSdk(request.getPreviousResourceTags()))
+ .resourceTags(new HashSet<>(Translator.translateTagsToSdk(request.getPreviousResourceState().getTags())))
+ .build();
+
+ final Tagging.TagSet desiredTags = Tagging.TagSet.builder()
+ .systemTags(Tagging.translateTagsToSdk(request.getSystemTags()))
+ .stackTags(Tagging.translateTagsToSdk(request.getDesiredResourceTags()))
+ .resourceTags(new HashSet<>(Translator.translateTagsToSdk(request.getDesiredResourceState().getTags())))
+ .build();
+
+ return ProgressEvent.progress(desiredModel, callbackContext)
+ .then(progress -> updateTags(proxy, proxyClient, progress, previousTags, desiredTags))
+ .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
+ }
+
+}
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
new file mode 100644
index 000000000..10ef93dac
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/AbstractHandlerTest.java
@@ -0,0 +1,206 @@
+package software.amazon.rds.integration;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.json.JSONObject;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsResponse;
+import software.amazon.awssdk.services.rds.model.Integration;
+import software.amazon.awssdk.services.rds.model.IntegrationStatus;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Credentials;
+import software.amazon.cloudformation.proxy.LoggerProxy;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import software.amazon.cloudformation.proxy.delay.Constant;
+import software.amazon.rds.common.error.ErrorCode;
+import software.amazon.rds.common.handler.HandlerConfig;
+import software.amazon.rds.common.handler.Tagging;
+import software.amazon.rds.test.common.core.AbstractTestBase;
+import software.amazon.rds.test.common.core.HandlerName;
+import software.amazon.rds.test.common.core.TestUtils;
+import software.amazon.rds.test.common.verification.AccessPermissionVerificationMode;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+public abstract class AbstractHandlerTest extends AbstractTestBase {
+
+ protected static final String LOGICAL_RESOURCE_IDENTIFIER = "integrationresource";
+
+ protected static final Credentials MOCK_CREDENTIALS;
+ protected static final org.slf4j.Logger delegate;
+ protected static final LoggerProxy logger;
+
+ static final Set TAG_LIST;
+ static final Set TAG_LIST_EMPTY;
+ static final Set TAG_LIST_ALTER;
+ static final Tagging.TagSet TAG_SET;
+
+ // use an accelerated backoff for faster unit testing
+ protected final HandlerConfig TEST_HANDLER_CONFIG = HandlerConfig.builder()
+ .probingEnabled(false)
+ .backoff(Constant.of().delay(Duration.ofMillis(1))
+ .timeout(Duration.ofSeconds(120))
+ .build())
+ .build();
+
+ static {
+ System.setProperty("org.slf4j.simpleLogger.showDateTime", "true");
+ System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "HH:mm:ss:SSS Z");
+ MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token");
+
+ delegate = LoggerFactory.getLogger("testing");
+ logger = new LoggerProxy();
+
+
+ TAG_LIST_EMPTY = ImmutableSet.of();
+
+ TAG_LIST = ImmutableSet.of(
+ Tag.builder().key("k1").value("kv1").build()
+ );
+
+ TAG_LIST_ALTER = ImmutableSet.of(
+ Tag.builder().key("k1").value("kv2").build(),
+ Tag.builder().key("k2").value("kv2").build(),
+ Tag.builder().key("k3").value("kv3").build()
+ );
+
+ TAG_SET = Tagging.TagSet.builder()
+ .systemTags(ImmutableSet.of(
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("system-tag-1").value("system-tag-value1").build(),
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("system-tag-2").value("system-tag-value2").build(),
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("system-tag-3").value("system-tag-value3").build()
+ )).stackTags(ImmutableSet.of(
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("stack-tag-1").value("stack-tag-value1").build(),
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("stack-tag-2").value("stack-tag-value2").build(),
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("stack-tag-3").value("stack-tag-value3").build()
+ )).resourceTags(ImmutableSet.of(
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("resource-tag-1").value("resource-tag-value1").build(),
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("resource-tag-2").value("resource-tag-value2").build(),
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("resource-tag-3").value("resource-tag-value3").build()
+ )).build();
+ }
+
+ protected static final String INTEGRATION_NAME = "integration-identifier-1";
+ protected static final String INTEGRATION_ARN = "arn:aws:rds:us-east-1:123456789012:integration:de4b78a2-0bff-4e93-814a-bacd3f81b383";
+ protected static final String SOURCE_ARN = "arn:aws:rds:us-east-1:123456789012:cluster:cfn-integ-test-prov-5-rdsdbcluster-ozajchztpipc";
+ protected static final String TARGET_ARN = "arn:aws:redshift:us-east-1:123456789012:namespace:ad99c581-dbac-4a1b-9602-d5c5e7f77b24";
+ protected static final String KMS_KEY_ID = "arn:aws:kms:us-east-1:123456789012:key/9d67ba2d-daca-4e3c-ac23-16342062ede3";
+ protected static final Map ADDITIONAL_ENCRYPTION_CONTEXT = ImmutableMap.of("eck1", "ecv1", "eck2", "ecv2");
+
+ static final Integration INTEGRATION_ACTIVE = Integration.builder()
+ .integrationArn(INTEGRATION_ARN)
+ .integrationName(INTEGRATION_NAME)
+ .sourceArn(SOURCE_ARN)
+ .targetArn(TARGET_ARN)
+ .kmsKeyId(KMS_KEY_ID)
+ .status(IntegrationStatus.ACTIVE)
+ .createTime(Instant.ofEpochMilli(1699489854712L))
+ .additionalEncryptionContext(ADDITIONAL_ENCRYPTION_CONTEXT)
+ .tags(toAPITags(TAG_LIST))
+ .build();
+
+
+ protected static final Integration INTEGRATION_CREATING = INTEGRATION_ACTIVE.toBuilder()
+ .status(IntegrationStatus.CREATING)
+ .build();
+
+ protected static final Integration INTEGRATION_FAILED = INTEGRATION_ACTIVE.toBuilder()
+ .status(IntegrationStatus.FAILED)
+ .build();
+
+ protected static final Integration INTEGRATION_DELETING = INTEGRATION_ACTIVE.toBuilder()
+ .status(IntegrationStatus.DELETING)
+ .build();
+
+ protected static final ResourceModel INTEGRATION_ACTIVE_MODEL = ResourceModel.builder()
+ .integrationArn(INTEGRATION_ARN)
+ .integrationName(INTEGRATION_NAME)
+ .sourceArn(SOURCE_ARN)
+ .targetArn(TARGET_ARN)
+ .kMSKeyId(KMS_KEY_ID)
+ .createTime("2023-11-09T00:30:54.712000+00:00")
+ .additionalEncryptionContext(ADDITIONAL_ENCRYPTION_CONTEXT)
+ .tags(TAG_LIST)
+ .build();
+
+ protected static final ResourceModel INTEGRATION_MODEL_WITH_NO_NAME = INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .integrationName(null)
+ .build();
+
+
+ static ProxyClient MOCK_PROXY(final AmazonWebServicesClientProxy proxy, final ClientT client) {
+ return new BaseProxyClient<>(proxy, client);
+ }
+
+ protected abstract BaseHandlerStd getHandler();
+
+ protected abstract AmazonWebServicesClientProxy getProxy();
+
+ protected abstract ProxyClient getRdsProxy();
+
+ public abstract HandlerName getHandlerName();
+
+ private static final JSONObject resourceSchema = new Configuration().resourceSchemaJsonObject();
+
+ public void verifyAccessPermissions(final Object mock) {
+ new AccessPermissionVerificationMode()
+ .withDefaultPermissions()
+ .withSchemaPermissions(resourceSchema, getHandlerName())
+ .verify(TestUtils.getVerificationData(mock));
+ }
+
+ @Override
+ protected ProgressEvent invokeHandleRequest(
+ final ResourceHandlerRequest request,
+ final CallbackContext context
+ ) {
+ return getHandler().handleRequest(getProxy(), request, context, getRdsProxy(), logger);
+ }
+
+ @Override
+ protected String getLogicalResourceIdentifier() {
+ return LOGICAL_RESOURCE_IDENTIFIER;
+ }
+
+ @Override
+ protected void expectResourceSupply(Supplier supplier) {
+ when(getRdsProxy().client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .then((req) ->
+ DescribeIntegrationsResponse.builder()
+ .integrations(supplier.get())
+ .build());
+ }
+
+ protected static Collection toAPITags(Collection tags) {
+ return tags.stream().map(t -> software.amazon.awssdk.services.rds.model.Tag.builder()
+ .key(t.getKey())
+ .value(t.getValue())
+ .build())
+ .collect(Collectors.toSet());
+ }
+
+ public static AwsServiceException makeAwsServiceException(ErrorCode errorCode) {
+ return AwsServiceException.builder()
+ .awsErrorDetails(
+ AwsErrorDetails.builder()
+ .errorCode(errorCode.toString())
+ .build())
+ .build();
+ }
+}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/BaseConfigurationTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/BaseConfigurationTest.java
new file mode 100644
index 000000000..1e244d6cd
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/BaseConfigurationTest.java
@@ -0,0 +1,25 @@
+package software.amazon.rds.integration;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static software.amazon.rds.integration.AbstractHandlerTest.INTEGRATION_ACTIVE_MODEL;
+
+public class BaseConfigurationTest {
+
+ @Test
+ public void resourceDefinedTags_withNullTags_returnEmpty() {
+ Configuration conf = new Configuration();
+ Assertions.assertNull(conf.resourceDefinedTags(INTEGRATION_ACTIVE_MODEL.toBuilder().tags(null).build()));
+ }
+
+ @Test
+ public void resourceDefinedTags_withNonNullTags_returnTags() {
+ Configuration conf = new Configuration();
+ Assertions.assertEquals(
+ ImmutableMap.of("k1", "kv1"),
+ conf.resourceDefinedTags(INTEGRATION_ACTIVE_MODEL)
+ );
+ }
+}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/BaseProxyClient.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/BaseProxyClient.java
new file mode 100644
index 000000000..98fd509d4
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/BaseProxyClient.java
@@ -0,0 +1,65 @@
+package software.amazon.rds.integration;
+
+import software.amazon.awssdk.awscore.AwsRequest;
+import software.amazon.awssdk.awscore.AwsResponse;
+import software.amazon.awssdk.core.ResponseBytes;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.pagination.sync.SdkIterable;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.ProxyClient;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+public class BaseProxyClient implements ProxyClient {
+
+ protected final AmazonWebServicesClientProxy proxy;
+ protected final ClientT client;
+
+ public BaseProxyClient(
+ final AmazonWebServicesClientProxy proxy,
+ final ClientT client
+ ) {
+ this.proxy = proxy;
+ this.client = client;
+ }
+
+ @Override
+ public ResponseT injectCredentialsAndInvokeV2(RequestT request,
+ Function requestFunction) {
+ return proxy.injectCredentialsAndInvokeV2(request, requestFunction);
+ }
+
+ @Override
+ public CompletableFuture injectCredentialsAndInvokeV2Async(
+ RequestT request,
+ Function> requestFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public > IterableT injectCredentialsAndInvokeIterableV2(
+ RequestT request,
+ Function requestFunction) {
+ return proxy.injectCredentialsAndInvokeIterableV2(request, requestFunction);
+ }
+
+ @Override
+ public ResponseInputStream injectCredentialsAndInvokeV2InputStream(
+ RequestT request,
+ Function> requestFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ResponseBytes injectCredentialsAndInvokeV2Bytes(
+ RequestT request,
+ Function> requestFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ClientT client() {
+ return client;
+ }
+}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/CreateHandlerTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/CreateHandlerTest.java
new file mode 100644
index 000000000..edfd62e1b
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/CreateHandlerTest.java
@@ -0,0 +1,232 @@
+package software.amazon.rds.integration;
+
+import lombok.Getter;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+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;
+import software.amazon.awssdk.services.rds.model.CreateIntegrationRequest;
+import software.amazon.awssdk.services.rds.model.CreateIntegrationResponse;
+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.cloudformation.exceptions.CfnNotStabilizedException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.rds.test.common.core.HandlerName;
+
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+
+@ExtendWith(MockitoExtension.class)
+public class CreateHandlerTest extends AbstractHandlerTest {
+
+ private final String DUPLICATE_INTEGRATION_ERROR_MESSAGE = "A zero-ETL integration named ct80e2519bfd4d4ad1bfdc943c66ce0482 " +
+ "already exists in account 123123123123. Integration names must be unique within an account." +
+ " Specify a different name for your integration, or delete the existing integration.";
+
+ @Mock
+ @Getter
+ RdsClient rdsClient;
+
+ @Mock
+ @Getter
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ @Getter
+ private ProxyClient rdsProxy;
+
+ @Getter
+ private CreateHandler handler;
+
+ @Override
+ public HandlerName getHandlerName() {
+ return HandlerName.CREATE;
+ }
+
+ @BeforeEach
+ public void setup() {
+ handler = new CreateHandler(TEST_HANDLER_CONFIG);
+ rdsClient = mock(RdsClient.class);
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ rdsProxy = MOCK_PROXY(proxy, rdsClient);
+ }
+
+ @AfterEach
+ public void tear_down() {
+ verify(rdsClient, atLeastOnce()).serviceName();
+ verifyNoMoreInteractions(rdsClient);
+ verifyAccessPermissions(rdsClient);
+ }
+
+
+ @Test
+ public void handleRequest_CreateIntegration_withAllFields_success() {
+ when(rdsProxy.client().createIntegration(any(CreateIntegrationRequest.class)))
+ .thenReturn(CreateIntegrationResponse.builder()
+ .build());
+
+
+ // Integration goes from CREATING -> ACTIVE, when everything is normal
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_CREATING);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ () -> {
+ if (transitions.size() > 0) {
+ return transitions.remove();
+ }
+ return INTEGRATION_ACTIVE;
+ },
+ () -> INTEGRATION_ACTIVE_MODEL,
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(1)).createIntegration(
+ ArgumentMatchers. argThat(req -> {
+ // TODO verify the content
+ return true;
+ })
+ );
+ verify(rdsProxy.client(), times(3)).describeIntegrations(
+ ArgumentMatchers.argThat(req ->
+ Objects.equals(INTEGRATION_CREATING.integrationArn(), req.integrationIdentifier())
+ )
+ );
+ }
+
+ @Test
+ public void handleRequest_CreateIntegration_withNoName_shouldGenerateName() {
+ when(rdsProxy.client().createIntegration(any(CreateIntegrationRequest.class)))
+ .thenReturn(CreateIntegrationResponse.builder()
+ .build());
+
+ // Integration goes from CREATING -> ACTIVE, when everything is normal
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_CREATING);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ () -> {
+ if (transitions.size() > 0) {
+ return transitions.remove();
+ }
+ return INTEGRATION_ACTIVE;
+ },
+ () -> INTEGRATION_MODEL_WITH_NO_NAME,
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(1)).createIntegration(
+ ArgumentMatchers. argThat(req -> req.integrationName().contains(LOGICAL_RESOURCE_IDENTIFIER))
+ );
+ verify(rdsProxy.client(), times(3)).describeIntegrations(
+ ArgumentMatchers.argThat(req ->
+ Objects.equals(INTEGRATION_CREATING.integrationArn(), req.integrationIdentifier())
+ )
+ );
+ }
+
+ @Test
+ public void handleRequest_CreateIntegration_withTerminalFailureState_returnFailure() {
+ when(rdsProxy.client().createIntegration(any(CreateIntegrationRequest.class)))
+ .thenReturn(CreateIntegrationResponse.builder()
+ .build());
+
+ // Integration goes from CREATING -> ACTIVE, when everything is normal
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_CREATING);
+ Assertions.assertThatThrownBy(() ->
+ test_handleRequest_base(
+ new CallbackContext(),
+ () -> {
+ if (transitions.size() > 0) {
+ return transitions.remove();
+ }
+ return INTEGRATION_FAILED;
+ },
+ () -> INTEGRATION_ACTIVE_MODEL, // unused
+ expectFailed(HandlerErrorCode.NotStabilized) // unused
+ )
+ ).isInstanceOf(CfnNotStabilizedException.class);
+
+ verify(rdsProxy.client(), times(1)).createIntegration(
+ ArgumentMatchers. argThat(req -> {
+ // TODO verify the content
+ return true;
+ })
+ );
+
+ verify(rdsProxy.client(), times(2)).describeIntegrations(
+ ArgumentMatchers.argThat(req ->
+ Objects.equals(INTEGRATION_CREATING.integrationArn(), req.integrationIdentifier())
+ )
+ );
+ }
+
+ @Test
+ public void handleRequest_CreateIntegration_withIntegrationAlreadyExistsException_returnFailure() {
+ when(rdsProxy.client().createIntegration(any(CreateIntegrationRequest.class)))
+ .thenThrow(IntegrationAlreadyExistsException.builder()
+ .message("Integration with the name already exists")
+ .build());
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ null,
+ () -> INTEGRATION_ACTIVE_MODEL,
+ expectFailed(HandlerErrorCode.AlreadyExists)
+ );
+
+ verify(rdsProxy.client(), times(1)).createIntegration(
+ ArgumentMatchers. argThat(req -> {
+ // TODO verify the content
+ return true;
+ })
+ );
+
+ }
+
+ @Test
+ public void handleRequest_CreateIntegration_withDuplicateIntegrationName_returnFailure() {
+ 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 -> {
+ // TODO verify the content
+ return true;
+ })
+ );
+
+ }
+}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/DeleteHandlerTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/DeleteHandlerTest.java
new file mode 100644
index 000000000..4c7fca481
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/DeleteHandlerTest.java
@@ -0,0 +1,202 @@
+package software.amazon.rds.integration;
+
+import lombok.Getter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.DeleteIntegrationRequest;
+import software.amazon.awssdk.services.rds.model.DeleteIntegrationResponse;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsResponse;
+import software.amazon.awssdk.services.rds.model.Integration;
+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.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.rds.common.error.ErrorCode;
+import software.amazon.rds.test.common.core.HandlerName;
+
+import java.time.Duration;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DeleteHandlerTest extends AbstractHandlerTest {
+
+ private static final String MSG_NOT_FOUND = "not found";
+
+ @Mock
+ @Getter
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ @Getter
+ private ProxyClient rdsProxy;
+
+ @Mock
+ @Getter
+ RdsClient rdsClient;
+
+ @Getter
+ private DeleteHandler handler;
+
+ @Override
+ public HandlerName getHandlerName() {
+ return HandlerName.DELETE;
+ }
+
+ private boolean expectServiceInvocation;
+
+ @BeforeEach
+ public void setup() {
+ handler = new DeleteHandler(TEST_HANDLER_CONFIG);
+ rdsClient = mock(RdsClient.class);
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ rdsProxy = MOCK_PROXY(proxy, rdsClient);
+ expectServiceInvocation = true;
+ }
+
+ @AfterEach
+ public void tear_down() {
+ if (expectServiceInvocation) {
+ verify(rdsClient, atLeastOnce()).serviceName();
+ }
+ verifyNoMoreInteractions(rdsClient);
+ verifyAccessPermissions(rdsClient);
+ }
+
+ @Test
+ public void handleRequest_deleting_should_fail_if_no_integration_found_before_first_call() {
+ when(rdsProxy.client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .then((a) -> {throw IntegrationNotFoundException.builder().message(MSG_NOT_FOUND).build();});
+
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ null,
+ () -> INTEGRATION_ACTIVE_MODEL, // unused
+ expectFailed(HandlerErrorCode.NotFound)
+ );
+ }
+
+ @Test
+ public void handleRequest_deleting_should_succeed() {
+ when(rdsProxy.client().deleteIntegration(any(DeleteIntegrationRequest.class)))
+ .thenReturn(DeleteIntegrationResponse.builder().build());
+
+ final Queue transitions = new ConcurrentLinkedQueue<>();
+ // first call is the check whether the resource exists
+ transitions.add(INTEGRATION_ACTIVE);
+ transitions.add(INTEGRATION_DELETING);
+ transitions.add(INTEGRATION_DELETING);
+
+ when(rdsProxy.client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .thenAnswer((a) -> {
+ if (transitions.size() > 0) {
+ return DescribeIntegrationsResponse.builder().integrations(transitions.remove()).build();
+ }
+ throw IntegrationNotFoundException.builder().message(MSG_NOT_FOUND).build();
+ });
+
+ ProgressEvent progressEvent = ProgressEvent.builder()
+ .callbackContext(new CallbackContext()).build();
+
+ final int DELAY = 6;
+ while (progressEvent.getCallbackContext().getDeleteWaitTime() <= 500) {
+ progressEvent = test_handleRequest_base(
+ progressEvent.getCallbackContext(),
+ null,
+ () -> INTEGRATION_ACTIVE_MODEL, // unused
+ expectInProgress(DELAY)
+ );
+ }
+ test_handleRequest_base(
+ progressEvent.getCallbackContext(),
+ null,
+ () -> INTEGRATION_ACTIVE_MODEL, // unused
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(1)).deleteIntegration(any(DeleteIntegrationRequest.class));
+ }
+
+ @Test
+ public void handleRequest_deleting_with_IntegrationNotFoundException_should_succeed() {
+ when(rdsProxy.client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .thenReturn(DescribeIntegrationsResponse.builder().integrations(INTEGRATION_ACTIVE).build());
+ when(rdsProxy.client().deleteIntegration(any(DeleteIntegrationRequest.class)))
+ .then((t) -> { throw IntegrationNotFoundException.builder().build(); });
+
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ null, // unused
+ () -> INTEGRATION_ACTIVE_MODEL, // unused
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(1)).deleteIntegration(any(DeleteIntegrationRequest.class));
+ }
+
+ @Test
+ public void handleRequest_deleting_with_internalerror_should_fail() {
+ when(rdsProxy.client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .thenReturn(DescribeIntegrationsResponse.builder().integrations(INTEGRATION_ACTIVE).build());
+ when(rdsProxy.client().deleteIntegration(any(DeleteIntegrationRequest.class)))
+ .then((t) -> makeAwsServiceException(ErrorCode.InternalFailure));
+
+ final Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+ transitions.add(INTEGRATION_DELETING);
+ transitions.add(INTEGRATION_DELETING);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ null, // unused
+ () -> INTEGRATION_ACTIVE_MODEL, // unused
+ expectFailed(HandlerErrorCode.InternalFailure)
+ );
+
+ verify(rdsProxy.client(), times(1)).deleteIntegration(any(DeleteIntegrationRequest.class));
+ }
+
+ @Test
+ public void handleRequest_deleting_should_fail_when_IntegrationConflictOperationException() {
+ when(rdsProxy.client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .thenReturn(DescribeIntegrationsResponse.builder().integrations(INTEGRATION_ACTIVE).build());
+ when(rdsProxy.client().deleteIntegration(any(DeleteIntegrationRequest.class)))
+ .then((invocationOnMock) -> {
+ throw IntegrationConflictOperationException.builder()
+ .message("STILL CREATING")
+ .build();
+ });
+
+ final Queue describeResponses = new ConcurrentLinkedQueue<>();
+ // this is called when stabilizing after the first lambda call
+ describeResponses.add(INTEGRATION_DELETING);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ null, // unused
+ () -> INTEGRATION_ACTIVE_MODEL, // unused
+ expectFailed(HandlerErrorCode.ResourceConflict)
+ );
+
+ // this call will cause IntegrationConflictOperationException
+ verify(rdsProxy.client(), times(1)).deleteIntegration(any(DeleteIntegrationRequest.class));
+ }
+}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/IntegrationStatusUtilTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/IntegrationStatusUtilTest.java
new file mode 100644
index 000000000..f050bc806
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/IntegrationStatusUtilTest.java
@@ -0,0 +1,32 @@
+package software.amazon.rds.integration;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.services.rds.model.IntegrationStatus;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class IntegrationStatusUtilTest {
+
+ @Test
+ public void assertKnownStatuses() {
+ // if this test fails, this means there is new status that this handler does not know about.
+ // In that case, check the new status, make any changes necessary to fix the handler, then
+ // add the state to the list of known states.
+ Set allStatuses = new HashSet<>(Arrays.asList(IntegrationStatus.values()));
+ allStatuses.remove(IntegrationStatus.UNKNOWN_TO_SDK_VERSION);
+ allStatuses.remove(IntegrationStatus.CREATING);
+ allStatuses.remove(IntegrationStatus.ACTIVE);
+ allStatuses.remove(IntegrationStatus.MODIFYING);
+ allStatuses.remove(IntegrationStatus.FAILED);
+ allStatuses.remove(IntegrationStatus.DELETING);
+ allStatuses.remove(IntegrationStatus.SYNCING);
+ allStatuses.remove(IntegrationStatus.NEEDS_ATTENTION);
+ Assertions.assertTrue(allStatuses.isEmpty(),
+ "There are new integration statuses that this handler does not know about: " +
+ allStatuses +
+ "Please check the handler code, and then add them to the list of known statuses");
+ }
+}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/ListHandlerTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/ListHandlerTest.java
new file mode 100644
index 000000000..14fb974d1
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/ListHandlerTest.java
@@ -0,0 +1,128 @@
+package software.amazon.rds.integration;
+
+import lombok.Getter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsResponse;
+import software.amazon.awssdk.services.rds.model.Integration;
+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.rds.common.error.ErrorCode;
+import software.amazon.rds.test.common.core.HandlerName;
+
+import java.time.Duration;
+import java.util.function.Supplier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class ListHandlerTest extends AbstractHandlerTest {
+
+ @Mock
+ @Getter
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ @Getter
+ private ProxyClient rdsProxy;
+
+ @Mock
+ RdsClient rdsClient;
+
+ @Getter
+ private ListHandler handler;
+
+ @Override
+ public HandlerName getHandlerName() {
+ return HandlerName.LIST;
+ }
+
+ private boolean expectServiceInvocation;
+
+ @BeforeEach
+ public void setup() {
+ handler = new ListHandler(TEST_HANDLER_CONFIG);
+ rdsClient = mock(RdsClient.class);
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ rdsProxy = MOCK_PROXY(proxy, rdsClient);
+ expectServiceInvocation = true;
+ }
+
+ @AfterEach
+ public void tear_down() {
+ if (expectServiceInvocation) {
+ verify(rdsClient, atLeastOnce()).serviceName();
+ }
+ verifyNoMoreInteractions(rdsClient);
+ verifyAccessPermissions(rdsClient);
+ }
+
+ @Test
+ public void handleRequest_Success() {
+ when(rdsProxy.client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .thenReturn(DescribeIntegrationsResponse.builder()
+ .integrations(INTEGRATION_ACTIVE)
+ .marker("marker2")
+ .build());
+
+ expectServiceInvocation = false;
+
+ final ProgressEvent response = test_handleRequest_base(
+ new CallbackContext(),
+ null,
+ () -> INTEGRATION_ACTIVE_MODEL,
+ expectSuccess()
+ );
+
+ assertThat(response.getResourceModel()).isNull();
+ assertThat(response.getResourceModels()).isNotNull();
+ assertThat(response.getResourceModels()).containsExactly(Translator.translateToModel(INTEGRATION_ACTIVE));
+ assertThat(response.getNextToken()).isEqualTo("marker2");
+
+ verify(rdsProxy.client(), times(1)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ }
+
+ @Test
+ public void handleRequest_onException_followsDefaultErrorChain() {
+ when(rdsProxy.client().describeIntegrations(any(DescribeIntegrationsRequest.class)))
+ .thenAnswer((a) -> AwsServiceException.builder()
+ .awsErrorDetails(AwsErrorDetails.builder()
+ .errorCode(ErrorCode.InternalFailure.toString())
+ .build())
+ .build());
+
+ expectServiceInvocation = false;
+
+ final ProgressEvent response = test_handleRequest_base(
+ new CallbackContext(),
+ null,
+ () -> INTEGRATION_ACTIVE_MODEL,
+ expectFailed(HandlerErrorCode.InternalFailure)
+ );
+
+ verify(rdsProxy.client(), times(1)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ }
+
+ @Override
+ protected void expectResourceSupply(Supplier supplier) {
+ // not used in this test
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/ReadHandlerTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/ReadHandlerTest.java
new file mode 100644
index 000000000..a461f60ed
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/ReadHandlerTest.java
@@ -0,0 +1,95 @@
+package software.amazon.rds.integration;
+
+import lombok.Getter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.rds.common.error.ErrorCode;
+import software.amazon.rds.test.common.core.HandlerName;
+
+import java.time.Duration;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+@ExtendWith(MockitoExtension.class)
+public class ReadHandlerTest extends AbstractHandlerTest {
+
+ @Mock
+ @Getter
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ @Getter
+ private ProxyClient rdsProxy;
+
+ @Mock
+ RdsClient rdsClient;
+
+ @Getter
+ private ReadHandler handler;
+
+ @Override
+ public HandlerName getHandlerName() {
+ return HandlerName.READ;
+ }
+
+ @BeforeEach
+ public void setup() {
+ handler = new ReadHandler(TEST_HANDLER_CONFIG);
+ rdsClient = mock(RdsClient.class);
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ rdsProxy = MOCK_PROXY(proxy, rdsClient);
+ }
+
+ @AfterEach
+ public void tear_down() {
+ verify(rdsClient, atLeastOnce()).serviceName();
+ verifyNoMoreInteractions(rdsClient);
+ verifyAccessPermissions(rdsClient);
+ }
+
+ @Test
+ void handleRequest_ReadSuccess() {
+ test_handleRequest_base(
+ new CallbackContext(),
+ () -> INTEGRATION_ACTIVE,
+ () -> INTEGRATION_ACTIVE_MODEL,
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(1))
+ .describeIntegrations(
+ Mockito.argThat(
+ req -> INTEGRATION_ARN.equals(req.integrationIdentifier()))
+ );
+ }
+
+ @Test
+ void handleRequest_ReadFailure() {
+ test_handleRequest_base(
+ new CallbackContext(),
+ () -> { throw makeAwsServiceException(ErrorCode.InternalFailure); },
+ () -> INTEGRATION_ACTIVE_MODEL,
+ expectFailed(HandlerErrorCode.ServiceInternalError)
+ );
+
+ verify(rdsProxy.client(), times(1))
+ .describeIntegrations(
+ Mockito.argThat(
+ req -> INTEGRATION_ARN.equals(req.integrationIdentifier()))
+ );
+ }
+}
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
new file mode 100644
index 000000000..fdbda7344
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/TranslatorTest.java
@@ -0,0 +1,57 @@
+package software.amazon.rds.integration;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.services.rds.model.DescribeIntegrationsRequest;
+import software.amazon.awssdk.services.rds.model.Filter;
+
+import java.util.List;
+
+@ExtendWith(MockitoExtension.class)
+public class TranslatorTest {
+
+ @Test
+ public void translateTags_withNullTags() {
+ Assertions.assertNull(Translator.translateTags(null));
+ }
+
+ @Test
+ public void translateTags_withNonNullTags() {
+ Assertions.assertEquals(
+ ImmutableSet.of(
+ Tag.builder().key("k1").value("v1").build(),
+ Tag.builder().key("k2").value("v2").build()
+ ),
+ Translator.translateTags(
+ ImmutableSet.of(
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("k1").value("v1").build(),
+ software.amazon.awssdk.services.rds.model.Tag.builder().key("k2").value("v2").build()
+ )
+ )
+ );
+ }
+
+ @Test
+ public void translateDescribeWithoutArn_shouldUseName() {
+ DescribeIntegrationsRequest request = Translator.describeIntegrationsRequest(
+ ResourceModel.builder()
+ .integrationName("integname12345").build()
+ );
+ Assertions.assertNull(request.integrationIdentifier());
+ List filters = request.filters();
+ Assertions.assertEquals(filters.size(), 1);
+ Assertions.assertEquals("integname12345", filters.get(0).name());
+ }
+
+ @Test
+ public void translateDescribeWithoutArnOrName_shouldThrow() {
+ Assertions.assertThrows(RuntimeException.class, () -> {
+ Translator.describeIntegrationsRequest(
+ ResourceModel.builder().build()
+ );
+ });
+ }
+}
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
new file mode 100644
index 000000000..43efd3dae
--- /dev/null
+++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/UpdateHandlerTest.java
@@ -0,0 +1,111 @@
+package software.amazon.rds.integration;
+
+import lombok.Getter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.services.rds.RdsClient;
+import software.amazon.awssdk.services.rds.model.AddTagsToResourceRequest;
+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.RemoveTagsFromResourceRequest;
+import software.amazon.awssdk.services.rds.model.RemoveTagsFromResourceResponse;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+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.Optional;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class UpdateHandlerTest extends AbstractHandlerTest {
+
+ private static final String RESOURCE_UPDATED_AT = "resource-updated-at";
+
+ @Mock
+ RdsClient rdsClient;
+
+ @Mock
+ @Getter
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ @Getter
+ private ProxyClient rdsProxy;
+
+ @Getter
+ private UpdateHandler handler;
+
+ private boolean expectServiceInvocation;
+
+ @Override
+ public HandlerName getHandlerName() {
+ return HandlerName.UPDATE;
+ }
+
+ @BeforeEach
+ public void setup() {
+ handler = new UpdateHandler(TEST_HANDLER_CONFIG);
+ rdsClient = mock(RdsClient.class);
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ rdsProxy = MOCK_PROXY(proxy, rdsClient);
+ expectServiceInvocation = true;
+ }
+
+ @AfterEach
+ public void tear_down() {
+ if (expectServiceInvocation) {
+ verify(rdsClient, atLeastOnce()).serviceName();
+ }
+ verifyNoMoreInteractions(rdsClient);
+ verifyAccessPermissions(rdsClient);
+ }
+
+ @Test
+ void handleRequest_Success() {
+ when(rdsProxy.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class)))
+ .thenReturn(RemoveTagsFromResourceResponse.builder().build());
+ when(rdsProxy.client().addTagsToResource(any(AddTagsToResourceRequest.class)))
+ .thenReturn(AddTagsToResourceResponse.builder().build());
+
+ Queue transitions = new ConcurrentLinkedQueue<>();
+ transitions.add(INTEGRATION_ACTIVE);
+
+ test_handleRequest_base(
+ new CallbackContext(),
+ ResourceHandlerRequest.builder()
+ .previousResourceTags(Translator.translateTagsToRequest(TAG_LIST))
+ .desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST_ALTER)),
+ () -> Optional.ofNullable(transitions.poll())
+ .orElse(INTEGRATION_ACTIVE
+ .toBuilder()
+ .tags(toAPITags(TAG_LIST_ALTER))
+ .build()),
+ () -> INTEGRATION_ACTIVE_MODEL,
+ () -> INTEGRATION_ACTIVE_MODEL.toBuilder()
+ .tags(TAG_LIST_ALTER)
+ .build(),
+ expectSuccess()
+ );
+
+ verify(rdsProxy.client(), times(2)).describeIntegrations(any(DescribeIntegrationsRequest.class));
+ verify(rdsProxy.client(), times(1)).removeTagsFromResource(any(RemoveTagsFromResourceRequest.class));
+ verify(rdsProxy.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class));
+ }
+
+}
diff --git a/aws-rds-integration/template.yml b/aws-rds-integration/template.yml
new file mode 100644
index 000000000..5daee0d72
--- /dev/null
+++ b/aws-rds-integration/template.yml
@@ -0,0 +1,23 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Transform: AWS::Serverless-2016-10-31
+Description: AWS SAM template for the AWS::RDS::Integration resource type
+
+Globals:
+ Function:
+ Timeout: 180 # docker start-up times can be long for SAM CLI
+ MemorySize: 512
+
+Resources:
+ TypeFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: software.amazon.rds.integration.HandlerWrapper::handleRequest
+ Runtime: java8
+ CodeUri: ./target/aws-rds-integration-handler-1.0-SNAPSHOT.jar
+
+ TestEntrypoint:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: software.amazon.rds.integration.HandlerWrapper::testEntrypoint
+ Runtime: java8
+ CodeUri: ./target/aws-rds-integration-handler-1.0-SNAPSHOT.jar
diff --git a/pom.xml b/pom.xml
index 4b78cf440..1051ec29c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,6 +18,7 @@
aws-rds-dbsubnetgroup
aws-rds-eventsubscription
aws-rds-globalcluster
+ aws-rds-integration
aws-rds-optiongroup
aws-rds-dbclusterendpoint