diff --git a/aws-msk-serverlessvpcconnection/.gitignore b/aws-msk-serverlessvpcconnection/.gitignore new file mode 100644 index 0000000..faa9259 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/.gitignore @@ -0,0 +1,23 @@ +# macOS +.DS_Store +._* + +# Maven outputs +.classpath + +# IntelliJ +*.iml +.idea +out.java +out/ +.settings +.project + +# auto-generated files +target/ + +# our logs +rpdk.log* + +# contains credentials +sam-tests/ diff --git a/aws-msk-serverlessvpcconnection/.rpdk-config b/aws-msk-serverlessvpcconnection/.rpdk-config new file mode 100644 index 0000000..f15657d --- /dev/null +++ b/aws-msk-serverlessvpcconnection/.rpdk-config @@ -0,0 +1,28 @@ +{ + "artifact_type": "RESOURCE", + "typeName": "AWS::MSK::ServerlessVpcConnection", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.msk.serverlessvpcconnection.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.msk.serverlessvpcconnection.HandlerWrapper::testEntrypoint", + "settings": { + "version": false, + "subparser_name": null, + "verbose": 0, + "force": false, + "type_name": null, + "artifact_type": null, + "endpoint_url": null, + "region": null, + "target_schemas": [], + "namespace": [ + "software", + "amazon", + "msk", + "serverlessvpcconnection" + ], + "codegen_template_path": "guided_aws", + "protocolVersion": "2.0.0" + }, + "executableEntrypoint": "software.amazon.msk.serverlessvpcconnection.HandlerWrapperExecutable" +} diff --git a/aws-msk-serverlessvpcconnection/README.md b/aws-msk-serverlessvpcconnection/README.md new file mode 100644 index 0000000..4b1410b --- /dev/null +++ b/aws-msk-serverlessvpcconnection/README.md @@ -0,0 +1,12 @@ +# AWS::MSK::ServerlessVpcConnection + +Congratulations on starting development! Next steps: + +1. Write the JSON schema describing your resource, `aws-msk-serverlessvpcconnection.json` +1. Implement your resource handlers. + +The RPDK will automatically generate the correct resource model from the schema whenever the project is built via Maven. You can also do this manually with the following command: `cfn generate`. + +> Please don't modify files under `target/generated-sources/rpdk`, as they will be automatically overwritten. + +The code uses [Lombok](https://projectlombok.org/), and [you may have to install IDE integrations](https://projectlombok.org/setup/overview) to enable auto-complete for Lombok-annotated classes. diff --git a/aws-msk-serverlessvpcconnection/aws-msk-serverlessvpcconnection.json b/aws-msk-serverlessvpcconnection/aws-msk-serverlessvpcconnection.json new file mode 100644 index 0000000..8a91c2c --- /dev/null +++ b/aws-msk-serverlessvpcconnection/aws-msk-serverlessvpcconnection.json @@ -0,0 +1,148 @@ +{ + "typeName": "AWS::MSK::ServerlessVpcConnection", + "description": "Resource Type definition for AWS::MSK::ServerlessVpcConnection", + "definitions": { + "Authentication": { + "type": "string", + "description": "The type of private link authentication", + "minLength": 3, + "maxLength": 10, + "enum": [ + "SASL_IAM" + ] + }, + "ClientSubnets": { + "type": "array", + "uniqueItems": false, + "insertionOrder": false, + "items": { + "type": "string", + "pattern": "^(subnet-)([a-z0-9]+)\\Z" + } + }, + "SecurityGroups": { + "type": "array", + "uniqueItems": false, + "insertionOrder": false, + "items": { + "type": "string", + "pattern": "^(sg-)([a-z0-9]+)\\Z" + } + }, + "Tags": { + "type": "object", + "description": "A key-value pair to associate with a resource.", + "patternProperties": { + "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$": { + "type": "string" + } + }, + "additionalProperties": false + }, + "VpcId": { + "type": "string", + "pattern": "^(vpc-)([a-z0-9]+)\\Z" + } + }, + "properties": { + "Arn": { + "type": "string" + }, + "Authentication": { + "$ref": "#/definitions/Authentication" + }, + "ClientSubnets": { + "$ref": "#/definitions/ClientSubnets" + }, + "TargetClusterArn": { + "description": "The Amazon Resource Name (ARN) of the target cluster", + "type": "string", + "pattern": "^arn:[\\w-]+:kafka:[\\w-]+:\\d+:cluster.*\\Z" + }, + "SecurityGroups": { + "$ref": "#/definitions/SecurityGroups" + }, + "Tags": { + "$ref": "#/definitions/Tags" + }, + "VpcId": { + "$ref": "#/definitions/VpcId" + } + }, + "additionalProperties": false, + "required": [ + "Authentication", + "ClientSubnets", + "SecurityGroups", + "TargetClusterArn", + "VpcId" + ], + "createOnlyProperties": [ + "/properties/ClientSubnets", + "/properties/Authentication", + "/properties/SecurityGroups", + "/properties/TargetClusterArn", + "/properties/VpcId" + ], + "readOnlyProperties": [ + "/properties/Arn" + ], + "primaryIdentifier": [ + "/properties/Arn" + ], + "tagging": { + "taggable": true, + "tagOnCreate": true, + "tagUpdatable": true, + "cloudFormationSystemTags": true, + "tagProperty": "/properties/Tags" + }, + "handlers": { + "create": { + "permissions": [ + "ec2:CreateVpcEndpoint", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeVpcAttribute", + "ec2:DescribeVpcs", + "ec2:DescribeVpcEndpoints", + "ec2:DescribeVpcEndpointConnections", + "ec2:CreateTags", + "iam:AttachRolePolicy", + "iam:CreateServiceLinkedRole", + "iam:PutRolePolicy", + "kafka:CreateVpcConnection", + "kafka:DescribeVpcConnection", + "kafka:TagResource" + ] + }, + "read": { + "permissions": [ + "kafka:DescribeVpcConnection" + ] + }, + "update": { + "permissions": [ + "kafka:DescribeVpcConnection", + "kafka:TagResource", + "kafka:UntagResource" + ] + }, + "delete": { + "permissions": [ + "ec2:DeleteVpcEndpoint", + "ec2:DeleteVpcEndpoints", + "ec2:DescribeVpcEndpoints", + "ec2:DescribeVpcEndpointConnections", + "kafka:DeleteVpcConnection", + "kafka:DescribeVpcConnection" + ] + }, + "list": { + "permissions": [ + "kafka:ListVpcConnections" + ] + } + } +} + diff --git a/aws-msk-serverlessvpcconnection/docs/README.md b/aws-msk-serverlessvpcconnection/docs/README.md new file mode 100644 index 0000000..7ec547a --- /dev/null +++ b/aws-msk-serverlessvpcconnection/docs/README.md @@ -0,0 +1,147 @@ +# AWS::MSK::ServerlessVpcConnection + +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::MSK::ServerlessVpcConnection",
+    "Properties" : {
+        "Title" : String,
+        "CoverSheetIncluded" : Boolean,
+        "DueDate" : String,
+        "ApprovalDate" : String,
+        "Memo" : Memo,
+        "SecondCopyOfMemo" : Memo,
+        "TestCode" : String,
+        "Authors" : [ String, ... ],
+        "Tags" : [ Tag, ... ]
+    }
+}
+
+ +### YAML + +
+Type: AWS::MSK::ServerlessVpcConnection
+Properties:
+    Title: String
+    CoverSheetIncluded: Boolean
+    DueDate: String
+    ApprovalDate: String
+    Memo: Memo
+    SecondCopyOfMemo: Memo
+    TestCode: String
+    Authors: 
+      - String
+    Tags: 
+      - Tag
+
+ +## Properties + +#### Title + +The title of the TPS report is a mandatory element. + +_Required_: Yes + +_Type_: String + +_Minimum_: 20 + +_Maximum_: 250 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### CoverSheetIncluded + +Required for all TPS Reports submitted after 2/19/1999 + +_Required_: No + +_Type_: Boolean + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### DueDate + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ApprovalDate + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Memo + +_Required_: No + +_Type_: Memo + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### SecondCopyOfMemo + +_Required_: No + +_Type_: Memo + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### TestCode + +_Required_: Yes + +_Type_: String + +_Allowed Values_: NOT_STARTED | CANCELLED + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Authors + +_Required_: No + +_Type_: List of String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### 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) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the TPSCode. + +### 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). + +#### TPSCode + +A TPS Code is automatically generated on creation and assigned as the unique identifier. + diff --git a/aws-msk-serverlessvpcconnection/docs/memo.md b/aws-msk-serverlessvpcconnection/docs/memo.md new file mode 100644 index 0000000..f100701 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/docs/memo.md @@ -0,0 +1,40 @@ +# AWS::MSK::ServerlessVpcConnection Memo + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Heading" : String,
+    "Body" : String
+}
+
+ +### YAML + +
+Heading: String
+Body: String
+
+ +## Properties + +#### Heading + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Body + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + diff --git a/aws-msk-serverlessvpcconnection/docs/tag.md b/aws-msk-serverlessvpcconnection/docs/tag.md new file mode 100644 index 0000000..b8df030 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/docs/tag.md @@ -0,0 +1,52 @@ +# AWS::MSK::ServerlessVpcConnection 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_: 1 + +_Maximum_: 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_: Yes + +_Type_: String + +_Maximum_: 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-msk-serverlessvpcconnection/example_inputs/inputs_1_create.json b/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_create.json new file mode 100644 index 0000000..46ed7da --- /dev/null +++ b/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_create.json @@ -0,0 +1,12 @@ +{ + "TPSCode": "...", + "Title": "...", + "CoverSheetIncluded": "...", + "DueDate": "...", + "ApprovalDate": "...", + "Memo": "...", + "SecondCopyOfMemo": "...", + "TestCode": "...", + "Authors": "...", + "Tags": "..." +} diff --git a/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_invalid.json b/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_invalid.json new file mode 100644 index 0000000..46ed7da --- /dev/null +++ b/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_invalid.json @@ -0,0 +1,12 @@ +{ + "TPSCode": "...", + "Title": "...", + "CoverSheetIncluded": "...", + "DueDate": "...", + "ApprovalDate": "...", + "Memo": "...", + "SecondCopyOfMemo": "...", + "TestCode": "...", + "Authors": "...", + "Tags": "..." +} diff --git a/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_update.json b/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_update.json new file mode 100644 index 0000000..46ed7da --- /dev/null +++ b/aws-msk-serverlessvpcconnection/example_inputs/inputs_1_update.json @@ -0,0 +1,12 @@ +{ + "TPSCode": "...", + "Title": "...", + "CoverSheetIncluded": "...", + "DueDate": "...", + "ApprovalDate": "...", + "Memo": "...", + "SecondCopyOfMemo": "...", + "TestCode": "...", + "Authors": "...", + "Tags": "..." +} diff --git a/aws-msk-serverlessvpcconnection/lombok.config b/aws-msk-serverlessvpcconnection/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-msk-serverlessvpcconnection/pom.xml b/aws-msk-serverlessvpcconnection/pom.xml new file mode 100644 index 0000000..9042b6d --- /dev/null +++ b/aws-msk-serverlessvpcconnection/pom.xml @@ -0,0 +1,237 @@ + + + 4.0.0 + + software.amazon.msk.serverlessvpcconnection + aws-msk-serverlessvpcconnection-handler + aws-msk-serverlessvpcconnection-handler + 1.0-SNAPSHOT + jar + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + + org.apache.logging.log4j + log4j-core + 2.17.1 + + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.17.1 + + + + + org.assertj + assertj-core + 3.12.2 + test + + + + org.junit.jupiter + junit-jupiter + 5.5.0-M1 + test + + + + org.mockito + mockito-core + 3.6.0 + test + + + + org.mockito + mockito-junit-jupiter + 3.6.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 + + + *:* + + **/Log4j2Plugins.dat + + + + + + + package + + shade + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + generate + generate-sources + + exec + + + cfn + generate ${cfn.generate.args} + ${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-msk-serverlessvpcconnection.json + + + + ${project.basedir}/target/loaded-target-schemas + + **/*.json + + + + + diff --git a/aws-msk-serverlessvpcconnection/resource-role.yaml b/aws-msk-serverlessvpcconnection/resource-role.yaml new file mode 100644 index 0000000..e349fc5 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/resource-role.yaml @@ -0,0 +1,42 @@ +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-MSK-ServerlessVpcConnection/* + Path: "/" + Policies: + - PolicyName: ResourceTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "initech:CreateReport" + - "initech:DeleteReport" + - "initech:DescribeReport" + - "initech:ListReports" + - "initech:UpdateReport" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/BaseHandlerStd.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/BaseHandlerStd.java new file mode 100644 index 0000000..2ef42d3 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/BaseHandlerStd.java @@ -0,0 +1,34 @@ +package software.amazon.msk.serverlessvpcconnection; + +import software.amazon.awssdk.core.SdkClient; +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; + +// Placeholder for the functionality that could be shared across Create/Read/Update/Delete/List Handlers + +public abstract class BaseHandlerStd extends BaseHandler { + @Override + public final ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + return handleRequest( + proxy, + request, + callbackContext != null ? callbackContext : new CallbackContext(), + proxy.newProxy(ClientBuilder::getClient), + logger + ); + } + + protected abstract ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger); +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/CallbackContext.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/CallbackContext.java new file mode 100644 index 0000000..0649768 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/CallbackContext.java @@ -0,0 +1,10 @@ +package software.amazon.msk.serverlessvpcconnection; + +import software.amazon.cloudformation.proxy.StdCallbackContext; + +@lombok.Getter +@lombok.Setter +@lombok.ToString +@lombok.EqualsAndHashCode(callSuper = true) +public class CallbackContext extends StdCallbackContext { +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ClientBuilder.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ClientBuilder.java new file mode 100644 index 0000000..f60e021 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ClientBuilder.java @@ -0,0 +1,25 @@ +package software.amazon.msk.serverlessvpcconnection; + +import software.amazon.awssdk.core.SdkClient; +// TODO: replace all usage of SdkClient with your service client type, e.g; YourServiceClient +// import software.amazon.awssdk.services.yourservice.YourServiceClient; +// import software.amazon.cloudformation.LambdaWrapper; + +public class ClientBuilder { + /* + TODO: uncomment the following, replacing YourServiceClient with your service client name + It is recommended to use static HTTP client so less memory is consumed + e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/master/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/ClientBuilder.java#L9 + + public static YourServiceClient getClient() { + return YourServiceClient.builder() + .httpClient(LambdaWrapper.HTTP_CLIENT) + .build(); + } + */ + + // TODO: remove this implementation once you have uncommented the above + public static SdkClient getClient() { + return null; + } +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/Configuration.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/Configuration.java new file mode 100644 index 0000000..daa749b --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/Configuration.java @@ -0,0 +1,8 @@ +package software.amazon.msk.serverlessvpcconnection; + +class Configuration extends BaseConfiguration { + + public Configuration() { + super("aws-msk-serverlessvpcconnection.json"); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/CreateHandler.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/CreateHandler.java new file mode 100644 index 0000000..0c1798c --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/CreateHandler.java @@ -0,0 +1,117 @@ +package software.amazon.msk.serverlessvpcconnection; + +// TODO: replace all usage of SdkClient with your service client type, e.g; YourServiceAsyncClient +// import software.amazon.awssdk.services.yourservice.YourServiceAsyncClient; + +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +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; + + +public class CreateHandler extends BaseHandlerStd { + private Logger logger; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + // TODO: Adjust Progress Chain according to your implementation + // https://github.com/aws-cloudformation/cloudformation-cli-java-plugin/blob/master/src/main/java/software/amazon/cloudformation/proxy/CallChain.java + + return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + + // STEP 1 [check if resource already exists] + // if target API does not support 'ResourceAlreadyExistsException' then following check is required + // for more information -> https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + .then(progress -> + // STEP 1.0 [initialize a proxy context] + // If your service API is not idempotent, meaning it does not distinguish duplicate create requests against some identifier (e.g; resource Name) + // and instead returns a 200 even though a resource already exists, you must first check if the resource exists here + // NOTE: If your service API throws 'ResourceAlreadyExistsException' for create requests this method is not necessary + proxy.initiate("AWS-MSK-ServerlessVpcConnection::Create::PreExistanceCheck", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + + // STEP 1.1 [TODO: construct a body of a request] + .translateToServiceRequest(Translator::translateToReadRequest) + + // STEP 1.2 [TODO: make an api call] + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + + // TODO: add custom read resource logic + + logger.log(String.format("%s has successfully been read.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + + // STEP 1.3 [TODO: handle exception] + .handleError((awsRequest, exception, client, model, context) -> { + // TODO: uncomment when ready to implement + // if (exception instanceof CfnNotFoundException) + // return ProgressEvent.progress(model, context); + // throw exception; + return ProgressEvent.progress(model, context); + }) + .progress() + ) + + // STEP 2 [create/stabilize progress chain - required for resource creation] + .then(progress -> + // If your service API throws 'ResourceAlreadyExistsException' for create requests then CreateHandler can return just proxy.initiate construction + // STEP 2.0 [initialize a proxy context] + // Implement client invocation of the create request through the proxyClient, which is already initialised with + // caller credentials, correct region and retry settings + proxy.initiate("AWS-MSK-ServerlessVpcConnection::Create", proxyClient,progress.getResourceModel(), progress.getCallbackContext()) + + // STEP 2.1 [TODO: construct a body of a request] + .translateToServiceRequest(Translator::translateToCreateRequest) + + // STEP 2.2 [TODO: make an api call] + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + try { + + // TODO: put your create resource code here + + } catch (final AwsServiceException e) { + /* + * While the handler contract states that the handler must always return a progress event, + * you may throw any instance of BaseHandlerException, as the wrapper map it to a progress event. + * Each BaseHandlerException maps to a specific error code, and you should map service exceptions as closely as possible + * to more specific error codes + */ + throw new CfnGeneralServiceException(ResourceModel.TYPE_NAME, e); + } + + logger.log(String.format("%s successfully created.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + + // STEP 2.3 [TODO: stabilize step is not necessarily required but typically involves describing the resource until it is in a certain status, though it can take many forms] + // for more information -> https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + // If your resource requires some form of stabilization (e.g. service does not provide strong consistency), you will need to ensure that your code + // accounts for any potential issues, so that a subsequent read/update requests will not cause any conflicts (e.g. NotFoundException/InvalidRequestException) + .stabilize((awsRequest, awsResponse, client, model, context) -> { + // TODO: put your stabilization code here + + final boolean stabilized = true; + logger.log(String.format("%s [%s] has been stabilized.", ResourceModel.TYPE_NAME, model.getPrimaryIdentifier())); + return stabilized; + }) + .progress() + ) + + // STEP 3 [TODO: describe call/chain to return the resource model] + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/DeleteHandler.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/DeleteHandler.java new file mode 100644 index 0000000..3f87d80 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/DeleteHandler.java @@ -0,0 +1,114 @@ +package software.amazon.msk.serverlessvpcconnection; + +// TODO: replace all usage of SdkClient with your service client type, e.g; YourServiceAsyncClient +// import software.amazon.awssdk.services.yourservice.YourServiceAsyncClient; + +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +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; + +public class DeleteHandler extends BaseHandlerStd { + private Logger logger; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + // TODO: Adjust Progress Chain according to your implementation + // https://github.com/aws-cloudformation/cloudformation-cli-java-plugin/blob/master/src/main/java/software/amazon/cloudformation/proxy/CallChain.java + + return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + + // STEP 1 [check if resource already exists] + // for more information -> https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + // if target API does not support 'ResourceNotFoundException' then following check is required + .then(progress -> + // STEP 1.0 [initialize a proxy context] + // If your service API does not return ResourceNotFoundException on delete requests against some identifier (e.g; resource Name) + // and instead returns a 200 even though a resource already deleted, you must first check if the resource exists here + // NOTE: If your service API throws 'ResourceNotFoundException' for delete requests this method is not necessary + proxy.initiate("AWS-MSK-ServerlessVpcConnection::Delete::PreDeletionCheck", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + + // STEP 1.1 [initialize a proxy context] + .translateToServiceRequest(Translator::translateToReadRequest) + + // STEP 1.2 [TODO: make an api call] + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + + // TODO: add custom read resource logic + + logger.log(String.format("%s has successfully been read.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + + // STEP 1.3 [TODO: handle exception] + .handleError((awsRequest, exception, client, model, context) -> { + // TODO: uncomment when ready to implement + // if (exception instanceof ResourceNotFoundException) + // return ProgressEvent.success(model, context); + // throw exception; + return ProgressEvent.progress(model, context); + }) + .progress() + ) + + // STEP 2.0 [delete/stabilize progress chain - required for resource deletion] + .then(progress -> + // If your service API throws 'ResourceNotFoundException' for delete requests then DeleteHandler can return just proxy.initiate construction + // STEP 2.0 [initialize a proxy context] + // Implement client invocation of the delete request through the proxyClient, which is already initialised with + // caller credentials, correct region and retry settings + proxy.initiate("AWS-MSK-ServerlessVpcConnection::Delete", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + + // STEP 2.1 [TODO: construct a body of a request] + .translateToServiceRequest(Translator::translateToDeleteRequest) + + // STEP 2.2 [TODO: make an api call] + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + try { + + // TODO: put your delete resource code here + + } catch (final AwsServiceException e) { + /* + * While the handler contract states that the handler must always return a progress event, + * you may throw any instance of BaseHandlerException, as the wrapper map it to a progress event. + * Each BaseHandlerException maps to a specific error code, and you should map service exceptions as closely as possible + * to more specific error codes + */ + throw new CfnGeneralServiceException(ResourceModel.TYPE_NAME, e); + } + + logger.log(String.format("%s successfully deleted.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + + // STEP 2.3 [TODO: stabilize step is not necessarily required but typically involves describing the resource until it is in a certain status, though it can take many forms] + // for more information -> https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + .stabilize((awsRequest, awsResponse, client, model, context) -> { + // TODO: put your stabilization code here + + final boolean stabilized = true; + logger.log(String.format("%s [%s] deletion has stabilized: %s", ResourceModel.TYPE_NAME, model.getPrimaryIdentifier(), stabilized)); + return stabilized; + }) + .progress() + ) + + // STEP 3 [TODO: return the successful progress event without resource model] + .then(progress -> ProgressEvent.defaultSuccessHandler(null)); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ListHandler.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ListHandler.java new file mode 100644 index 0000000..37c860e --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ListHandler.java @@ -0,0 +1,43 @@ +package software.amazon.msk.serverlessvpcconnection; + +import software.amazon.awssdk.awscore.AwsRequest; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import java.util.ArrayList; +import java.util.List; + +public class ListHandler extends BaseHandler { + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + final List models = new ArrayList<>(); + + // STEP 1 [TODO: construct a body of a request] + final AwsRequest awsRequest = Translator.translateToListRequest(request.getNextToken()); + + // STEP 2 [TODO: make an api call] + AwsResponse awsResponse = null; // proxy.injectCredentialsAndInvokeV2(awsRequest, ClientBuilder.getClient()::describeLogGroups); + + // STEP 3 [TODO: get a token for the next page] + String nextToken = null; + + // STEP 4 [TODO: construct resource models] + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/master/aws-logs-loggroup/src/main/java/software/amazon/logs/loggroup/ListHandler.java#L19-L21 + + return ProgressEvent.builder() + .resourceModels(models) + .nextToken(nextToken) + .status(OperationStatus.SUCCESS) + .build(); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ReadHandler.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ReadHandler.java new file mode 100644 index 0000000..63baa6d --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/ReadHandler.java @@ -0,0 +1,67 @@ +package software.amazon.msk.serverlessvpcconnection; + +// TODO: replace all usage of SdkClient with your service client type, e.g; YourServiceAsyncClient +// import software.amazon.awssdk.services.yourservice.YourServiceAsyncClient; + +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +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; + +public class ReadHandler extends BaseHandlerStd { + private Logger logger; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + // TODO: Adjust Progress Chain according to your implementation + // https://github.com/aws-cloudformation/cloudformation-cli-java-plugin/blob/master/src/main/java/software/amazon/cloudformation/proxy/CallChain.java + + // STEP 1 [initialize a proxy context] + return proxy.initiate("AWS-MSK-ServerlessVpcConnection::Read", proxyClient, request.getDesiredResourceState(), callbackContext) + + // STEP 2 [TODO: construct a body of a request] + .translateToServiceRequest(Translator::translateToReadRequest) + + // STEP 3 [TODO: make an api call] + // Implement client invocation of the read request through the proxyClient, which is already initialised with + // caller credentials, correct region and retry settings + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + try { + + // TODO: add custom read resource logic + // If describe request does not return ResourceNotFoundException, you must throw ResourceNotFoundException based on + // awsResponse values + + } catch (final AwsServiceException e) { // ResourceNotFoundException + /* + * While the handler contract states that the handler must always return a progress event, + * you may throw any instance of BaseHandlerException, as the wrapper map it to a progress event. + * Each BaseHandlerException maps to a specific error code, and you should map service exceptions as closely as possible + * to more specific error codes + */ + throw new CfnGeneralServiceException(ResourceModel.TYPE_NAME, e); // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/commit/2077c92299aeb9a68ae8f4418b5e932b12a8b186#diff-5761e3a9f732dc1ef84103dc4bc93399R56-R63 + } + + logger.log(String.format("%s has successfully been read.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + + // STEP 4 [TODO: gather all properties of the resource] + // Implement client invocation of the read request through the proxyClient, which is already initialised with + // caller credentials, correct region and retry settings + .done(awsResponse -> ProgressEvent.defaultSuccessHandler(Translator.translateFromReadResponse(awsResponse))); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/TagHelper.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/TagHelper.java new file mode 100644 index 0000000..c71570f --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/TagHelper.java @@ -0,0 +1,237 @@ +package software.amazon.msk.serverlessvpcconnection; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.Sets; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.ObjectUtils; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.SdkClient; +// TODO: Critical! Please replace the CloudFormation Tag model below with your service's own SDK Tag model +import software.amazon.awssdk.services.cloudformation.model.Tag; +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; + +public class TagHelper { + /** + * convertToMap + * + * Converts a collection of Tag objects to a tag-name -> tag-value map. + * + * Note: Tag objects with null tag values will not be included in the output + * map. + * + * @param tags Collection of tags to convert + * @return Converted Map of tags + */ + public static Map convertToMap(final Collection tags) { + if (CollectionUtils.isEmpty(tags)) { + return Collections.emptyMap(); + } + return tags.stream() + .filter(tag -> tag.value() != null) + .collect(Collectors.toMap( + Tag::key, + Tag::value, + (oldValue, newValue) -> newValue)); + } + + /** + * convertToSet + * + * Converts a tag map to a set of Tag objects. + * + * Note: Like convertToMap, convertToSet filters out value-less tag entries. + * + * @param tagMap Map of tags to convert + * @return Set of Tag objects + */ + public static Set convertToSet(final Map tagMap) { + if (MapUtils.isEmpty(tagMap)) { + return Collections.emptySet(); + } + return tagMap.entrySet().stream() + .filter(tag -> tag.getValue() != null) + .map(tag -> Tag.builder() + .key(tag.getKey()) + .value(tag.getValue()) + .build()) + .collect(Collectors.toSet()); + } + + /** + * shouldUpdateTags + * + * Determines whether user defined tags have been changed during update. + */ + public final boolean shouldUpdateTags(final ResourceModel resourceModel, final ResourceHandlerRequest handlerRequest) { + final Map previousTags = getPreviouslyAttachedTags(handlerRequest); + final Map desiredTags = getNewDesiredTags(handlerRequest); + return ObjectUtils.notEqual(previousTags, desiredTags); + } + + /** + * getPreviouslyAttachedTags + * + * If stack tags and resource tags are not merged together in Configuration class, + * we will get previously attached system (with `aws:cloudformation` prefix) and user defined tags from + * handlerRequest.getPreviousSystemTags() (system tags), + * handlerRequest.getPreviousResourceTags() (stack tags), + * handlerRequest.getPreviousResourceState().getTags() (resource tags). + * + * System tags are an optional feature. Merge them to your tags if you have enabled them for your resource. + * System tags can change on resource update if the resource is imported to the stack. + */ + public Map getPreviouslyAttachedTags(final ResourceHandlerRequest handlerRequest) { + final Map previousTags = new HashMap<>(); + + // TODO: get previous system tags if your service supports CloudFormation system tags + // if (handlerRequest.getPreviousSystemTags() != null) { + // previousTags.putAll(handlerRequest.getPreviousSystemTags()); + // } + + // get previous stack level tags from handlerRequest + if (handlerRequest.getPreviousResourceTags() != null) { + previousTags.putAll(handlerRequest.getPreviousResourceTags()); + } + + // TODO: get resource level tags from previous resource state based on your tag property name + // TODO: previousTags.putAll(handlerRequest.getPreviousResourceState().getTags()); // if tags are not null + return previousTags; + } + + /** + * getNewDesiredTags + * + * If stack tags and resource tags are not merged together in Configuration class, + * we will get new desired system (with `aws:cloudformation` prefix) and user defined tags from + * handlerRequest.getSystemTags() (system tags), + * handlerRequest.getDesiredResourceTags() (stack tags), + * handlerRequest.getDesiredResourceState().getTags() (resource tags). + * + * System tags are an optional feature. Merge them to your tags if you have enabled them for your resource. + * System tags can change on resource update if the resource is imported to the stack. + */ + public Map getNewDesiredTags(final ResourceHandlerRequest handlerRequest) { + final Map desiredTags = new HashMap<>(); + + // TODO: merge system tags with desired resource tags if your service supports CloudFormation system tags + // if (handlerRequest.getSystemTags() != null) { + // desiredTags.putAll(handlerRequest.getSystemTags()); + // } + + // get desired stack level tags from handlerRequest + if (handlerRequest.getDesiredResourceTags() != null) { + desiredTags.putAll(handlerRequest.getDesiredResourceTags()); + } + + // TODO: get resource level tags from resource model based on your tag property name + // TODO: desiredTags.putAll(convertToMap(handlerRequest.getDesiredResourceState().getTags())); // if tags are not null + return desiredTags; + } + + /** + * generateTagsToAdd + * + * Determines the tags the customer desired to define or redefine. + */ + public Map generateTagsToAdd(final Map previousTags, final Map desiredTags) { + return desiredTags.entrySet().stream() + .filter(e -> !previousTags.containsKey(e.getKey()) || !Objects.equals(previousTags.get(e.getKey()), e.getValue())) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue)); + } + + /** + * getTagsToRemove + * + * Determines the tags the customer desired to remove from the function. + */ + public Set generateTagsToRemove(final Map previousTags, final Map desiredTags) { + final Set desiredTagNames = desiredTags.keySet(); + + return previousTags.keySet().stream() + .filter(tagName -> !desiredTagNames.contains(tagName)) + .collect(Collectors.toSet()); + } + + /** + * generateTagsToAdd + * + * Determines the tags the customer desired to define or redefine. + */ + public Set generateTagsToAdd(final Set previousTags, final Set desiredTags) { + return Sets.difference(new HashSet<>(desiredTags), new HashSet<>(previousTags)); + } + + /** + * getTagsToRemove + * + * Determines the tags the customer desired to remove from the function. + */ + public Set generateTagsToRemove(final Set previousTags, final Set desiredTags) { + return Sets.difference(new HashSet<>(previousTags), new HashSet<>(desiredTags)); + } + + + /** + * tagResource during update + * + * Calls the service:TagResource API. + */ + private ProgressEvent + tagResource(final AmazonWebServicesClientProxy proxy, final ProxyClient serviceClient, final ResourceModel resourceModel, + final ResourceHandlerRequest handlerRequest, final CallbackContext callbackContext, final Map addedTags, final Logger logger) { + // TODO: add log for adding tags to resources during update + // e.g. logger.log(String.format("[UPDATE][IN PROGRESS] Going to add tags for ... resource: %s with AccountId: %s", + // resourceModel.getResourceName(), handlerRequest.getAwsAccountId())); + + // TODO: change untagResource in the method to your service API according to your SDK + return proxy.initiate("AWS-MSK-ServerlessVpcConnection::TagOps", serviceClient, resourceModel, callbackContext) + .translateToServiceRequest(model -> + Translator.tagResourceRequest(model, addedTags)) + .makeServiceCall((request, client) -> { + return (AwsResponse) null; + // TODO: replace the return null with your invoke log to call tagResource API to add tags + // e.g. proxy.injectCredentialsAndInvokeV2(request, client.client()::tagResource)) + }) + .progress(); + } + + /** + * untagResource during update + * + * Calls the service:UntagResource API. + */ + private ProgressEvent + untagResource(final AmazonWebServicesClientProxy proxy, final ProxyClient serviceClient, final ResourceModel resourceModel, + final ResourceHandlerRequest handlerRequest, final CallbackContext callbackContext, final Set removedTags, final Logger logger) { + // TODO: add log for removing tags from resources during update + // e.g. logger.log(String.format("[UPDATE][IN PROGRESS] Going to remove tags for ... resource: %s with AccountId: %s", + // resourceModel.getResourceName(), handlerRequest.getAwsAccountId())); + + // TODO: change untagResource in the method to your service API according to your SDK + return proxy.initiate("AWS-MSK-ServerlessVpcConnection::TagOps", serviceClient, resourceModel, callbackContext) + .translateToServiceRequest(model -> + Translator.untagResourceRequest(model, removedTags)) + .makeServiceCall((request, client) -> { + return (AwsResponse) null; + // TODO: replace the return null with your invoke log to call untag API to remove tags + // e.g. proxy.injectCredentialsAndInvokeV2(request, client.client()::untagResource) + }) + .progress(); + } + +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/Translator.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/Translator.java new file mode 100644 index 0000000..d325dcf --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/Translator.java @@ -0,0 +1,150 @@ +package software.amazon.msk.serverlessvpcconnection; + +import com.google.common.collect.Lists; +import software.amazon.awssdk.awscore.AwsRequest; +import software.amazon.awssdk.awscore.AwsResponse; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This class is a centralized placeholder for + * - api request construction + * - object translation to/from aws sdk + * - resource model construction for read/list handlers + */ + +public class Translator { + + /** + * Request to create a resource + * @param model resource model + * @return awsRequest the aws service request to create a resource + */ + static AwsRequest translateToCreateRequest(final ResourceModel model) { + final AwsRequest awsRequest = null; + // TODO: construct a request + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L39-L43 + return awsRequest; + } + + /** + * Request to read a resource + * @param model resource model + * @return awsRequest the aws service request to describe a resource + */ + static AwsRequest translateToReadRequest(final ResourceModel model) { + final AwsRequest awsRequest = null; + // TODO: construct a request + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L20-L24 + return awsRequest; + } + + /** + * Translates resource object from sdk into a resource model + * @param awsResponse the aws service describe resource response + * @return model resource model + */ + static ResourceModel translateFromReadResponse(final AwsResponse awsResponse) { + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L58-L73 + return ResourceModel.builder() + //.someProperty(response.property()) + .build(); + } + + /** + * Request to delete a resource + * @param model resource model + * @return awsRequest the aws service request to delete a resource + */ + static AwsRequest translateToDeleteRequest(final ResourceModel model) { + final AwsRequest awsRequest = null; + // TODO: construct a request + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L33-L37 + return awsRequest; + } + + /** + * Request to update properties of a previously created resource + * @param model resource model + * @return awsRequest the aws service request to modify a resource + */ + static AwsRequest translateToFirstUpdateRequest(final ResourceModel model) { + final AwsRequest awsRequest = null; + // TODO: construct a request + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L45-L50 + return awsRequest; + } + + /** + * Request to update some other properties that could not be provisioned through first update request + * @param model resource model + * @return awsRequest the aws service request to modify a resource + */ + static AwsRequest translateToSecondUpdateRequest(final ResourceModel model) { + final AwsRequest awsRequest = null; + // TODO: construct a request + return awsRequest; + } + + /** + * Request to list resources + * @param nextToken token passed to the aws service list resources request + * @return awsRequest the aws service request to list resources within aws account + */ + static AwsRequest translateToListRequest(final String nextToken) { + final AwsRequest awsRequest = null; + // TODO: construct a request + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L26-L31 + return awsRequest; + } + + /** + * Translates resource objects from sdk into a resource model (primary identifier only) + * @param awsResponse the aws service describe resource response + * @return list of resource models + */ + static List translateFromListRequest(final AwsResponse awsResponse) { + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L75-L82 + return streamOfOrEmpty(Lists.newArrayList()) + .map(resource -> ResourceModel.builder() + // include only primary identifier + .build()) + .collect(Collectors.toList()); + } + + private static Stream streamOfOrEmpty(final Collection collection) { + return Optional.ofNullable(collection) + .map(Collection::stream) + .orElseGet(Stream::empty); + } + + /** + * Request to add tags to a resource + * @param model resource model + * @return awsRequest the aws service request to create a resource + */ + static AwsRequest tagResourceRequest(final ResourceModel model, final Map addedTags) { + final AwsRequest awsRequest = null; + // TODO: construct a request + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L39-L43 + return awsRequest; + } + + /** + * Request to add tags to a resource + * @param model resource model + * @return awsRequest the aws service request to create a resource + */ + static AwsRequest untagResourceRequest(final ResourceModel model, final Set removedTags) { + final AwsRequest awsRequest = null; + // TODO: construct a request + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L39-L43 + return awsRequest; + } +} diff --git a/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/UpdateHandler.java b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/UpdateHandler.java new file mode 100644 index 0000000..5f07e78 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/main/java/software/amazon/msk/serverlessvpcconnection/UpdateHandler.java @@ -0,0 +1,140 @@ +package software.amazon.msk.serverlessvpcconnection; + +// TODO: replace all usage of SdkClient with your service client type, e.g; YourServiceAsyncClient +// import software.amazon.awssdk.services.yourservice.YourServiceAsyncClient; + +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +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; + +public class UpdateHandler extends BaseHandlerStd { + private Logger logger; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + + // TODO: Adjust Progress Chain according to your implementation + // https://github.com/aws-cloudformation/cloudformation-cli-java-plugin/blob/master/src/main/java/software/amazon/cloudformation/proxy/CallChain.java + + return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + + // STEP 1 [check if resource already exists] + // for more information -> https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + // if target API does not support 'ResourceNotFoundException' then following check is required + .then(progress -> + // STEP 1.0 [initialize a proxy context] + // If your service API does not return ResourceNotFoundException on update requests against some identifier (e.g; resource Name) + // and instead returns a 200 even though a resource does not exist, you must first check if the resource exists here + // NOTE: If your service API throws 'ResourceNotFoundException' for update requests this method is not necessary + proxy.initiate("AWS-MSK-ServerlessVpcConnection::Update::PreUpdateCheck", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + + // STEP 1.1 [initialize a proxy context] + .translateToServiceRequest(Translator::translateToReadRequest) + + // STEP 1.2 [TODO: make an api call] + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + + // TODO: add custom read resource logic + // If describe request does not return ResourceNotFoundException, you must throw ResourceNotFoundException based on + // awsResponse values + + logger.log(String.format("%s has successfully been read.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + .progress() + ) + + // STEP 2 [first update/stabilize progress chain - required for resource update] + .then(progress -> + // STEP 2.0 [initialize a proxy context] + // Implement client invocation of the update request through the proxyClient, which is already initialised with + // caller credentials, correct region and retry settings + proxy.initiate("AWS-MSK-ServerlessVpcConnection::Update::first", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + + // STEP 2.1 [TODO: construct a body of a request] + .translateToServiceRequest(Translator::translateToFirstUpdateRequest) + + // STEP 2.2 [TODO: make an api call] + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + try { + + // TODO: put your update resource code here + + } catch (final AwsServiceException e) { + /* + * While the handler contract states that the handler must always return a progress event, + * you may throw any instance of BaseHandlerException, as the wrapper map it to a progress event. + * Each BaseHandlerException maps to a specific error code, and you should map service exceptions as closely as possible + * to more specific error codes + */ + throw new CfnGeneralServiceException(ResourceModel.TYPE_NAME, e); + } + + logger.log(String.format("%s has successfully been updated.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + + // STEP 2.3 [TODO: stabilize step is not necessarily required but typically involves describing the resource until it is in a certain status, though it can take many forms] + // stabilization step may or may not be needed after each API call + // for more information -> https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + .stabilize((awsRequest, awsResponse, client, model, context) -> { + // TODO: put your stabilization code here + + final boolean stabilized = true; + + logger.log(String.format("%s [%s] update has stabilized: %s", ResourceModel.TYPE_NAME, model.getPrimaryIdentifier(), stabilized)); + return stabilized; + }) + .progress()) + + // If your resource is provisioned through multiple API calls, then the following pattern is required (and might take as many postUpdate callbacks as necessary) + // STEP 3 [second update/stabilize progress chain] + .then(progress -> + // STEP 3.0 [initialize a proxy context] + // If your resource is provisioned through multiple API calls, you will need to apply each subsequent update + // step in a discrete call/stabilize chain to ensure the entire resource is provisioned as intended. + proxy.initiate("AWS-MSK-ServerlessVpcConnection::Update::second", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + + // STEP 3.1 [TODO: construct a body of a request] + .translateToServiceRequest(Translator::translateToSecondUpdateRequest) + + // STEP 3.2 [TODO: make an api call] + .makeServiceCall((awsRequest, client) -> { + AwsResponse awsResponse = null; + try { + + // TODO: put your post update resource code here + + } catch (final AwsServiceException e) { + /* + * While the handler contract states that the handler must always return a progress event, + * you may throw any instance of BaseHandlerException, as the wrapper map it to a progress event. + * Each BaseHandlerException maps to a specific error code, and you should map service exceptions as closely as possible + * to more specific error codes + */ + throw new CfnGeneralServiceException(ResourceModel.TYPE_NAME, e); + } + + logger.log(String.format("%s has successfully been updated.", ResourceModel.TYPE_NAME)); + return awsResponse; + }) + .progress()) + + // STEP 4 [TODO: describe call/chain to return the resource model] + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/resources/log4j2.xml b/aws-msk-serverlessvpcconnection/src/resources/log4j2.xml new file mode 100644 index 0000000..5657daf --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/AbstractTestBase.java b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/AbstractTestBase.java new file mode 100644 index 0000000..f8c69ec --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/AbstractTestBase.java @@ -0,0 +1,66 @@ +package software.amazon.msk.serverlessvpcconnection; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import software.amazon.awssdk.awscore.AwsRequest; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.SdkClient; +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.Credentials; +import software.amazon.cloudformation.proxy.LoggerProxy; +import software.amazon.cloudformation.proxy.ProxyClient; + +public class AbstractTestBase { + protected static final Credentials MOCK_CREDENTIALS; + protected static final LoggerProxy logger; + + static { + MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token"); + logger = new LoggerProxy(); + } + static ProxyClient MOCK_PROXY( + final AmazonWebServicesClientProxy proxy, + final SdkClient sdkClient) { + return new ProxyClient() { + @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 requestT, Function> function) { + throw new UnsupportedOperationException(); + } + + @Override + public ResponseBytes + injectCredentialsAndInvokeV2Bytes(RequestT requestT, Function> function) { + throw new UnsupportedOperationException(); + } + + @Override + public SdkClient client() { + return sdkClient; + } + }; + } +} diff --git a/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/CreateHandlerTest.java b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/CreateHandlerTest.java new file mode 100644 index 0000000..344634b --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/CreateHandlerTest.java @@ -0,0 +1,68 @@ +package software.amazon.msk.serverlessvpcconnection; + +import java.time.Duration; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +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 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@ExtendWith(MockitoExtension.class) +public class CreateHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + SdkClient sdkClient; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(SdkClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + final CreateHandler handler = new CreateHandler(); + + final ResourceModel model = ResourceModel.builder().build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/DeleteHandlerTest.java b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/DeleteHandlerTest.java new file mode 100644 index 0000000..789b45b --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/DeleteHandlerTest.java @@ -0,0 +1,68 @@ +package software.amazon.msk.serverlessvpcconnection; + +import java.time.Duration; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +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 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@ExtendWith(MockitoExtension.class) +public class DeleteHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + SdkClient sdkClient; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(SdkClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + final DeleteHandler handler = new DeleteHandler(); + + final ResourceModel model = ResourceModel.builder().build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/ListHandlerTest.java b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/ListHandlerTest.java new file mode 100644 index 0000000..46ff2d6 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/ListHandlerTest.java @@ -0,0 +1,54 @@ +package software.amazon.msk.serverlessvpcconnection; + +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.ResourceHandlerRequest; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ListHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ListHandler handler = new ListHandler(); + + final ResourceModel model = ResourceModel.builder().build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = + handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).isNotNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/ReadHandlerTest.java b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/ReadHandlerTest.java new file mode 100644 index 0000000..e8f4399 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/ReadHandlerTest.java @@ -0,0 +1,68 @@ +package software.amazon.msk.serverlessvpcconnection; + +import java.time.Duration; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +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 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@ExtendWith(MockitoExtension.class) +public class ReadHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + SdkClient sdkClient; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(SdkClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ReadHandler handler = new ReadHandler(); + + final ResourceModel model = ResourceModel.builder().build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } +} diff --git a/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/UpdateHandlerTest.java b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/UpdateHandlerTest.java new file mode 100644 index 0000000..3579ba9 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/src/test/java/software/amazon/msk/serverlessvpcconnection/UpdateHandlerTest.java @@ -0,0 +1,68 @@ +package software.amazon.msk.serverlessvpcconnection; + +import java.time.Duration; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +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 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@ExtendWith(MockitoExtension.class) +public class UpdateHandlerTest extends AbstractTestBase { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + SdkClient sdkClient; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(SdkClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + final UpdateHandler handler = new UpdateHandler(); + + final ResourceModel model = ResourceModel.builder().build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } +} diff --git a/aws-msk-serverlessvpcconnection/template.yml b/aws-msk-serverlessvpcconnection/template.yml new file mode 100644 index 0000000..eb478b5 --- /dev/null +++ b/aws-msk-serverlessvpcconnection/template.yml @@ -0,0 +1,24 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::MSK::ServerlessVpcConnection resource type + +Globals: + Function: + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 256 + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.msk.serverlessvpcconnection.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-msk-serverlessvpcconnection-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.msk.serverlessvpcconnection.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-msk-serverlessvpcconnection-handler-1.0-SNAPSHOT.jar +