From 1be3442dcec285f98926cc1fba4ff756a93b517d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 1 Nov 2018 12:42:49 +0200 Subject: [PATCH] feat(aws-lambda): high level API for event sources (#1063) Adds a uniform high-level API for AWS Lambda event sources. This API allows developers to use a single API `lambda.addEventSource(source)` to connect a function with any type of event source. The `IEventSource` class is defined in the aws-lambda module and the aws-lambda-event-sources module includes implementations for various event sources. Each implementation utilizes the relevant mechanism. Currently supported: - SQS - SNS - S3 We eventually want to support all event sources as described in the Lambda developer guide. --- .../aws-lambda-event-sources/.gitignore | 17 ++ .../aws-lambda-event-sources/.npmignore | 16 ++ .../@aws-cdk/aws-lambda-event-sources/LICENSE | 201 ++++++++++++++++++ .../@aws-cdk/aws-lambda-event-sources/NOTICE | 2 + .../aws-lambda-event-sources/README.md | 102 +++++++++ .../aws-lambda-event-sources/lib/index.ts | 3 + .../aws-lambda-event-sources/lib/s3.ts | 33 +++ .../aws-lambda-event-sources/lib/sns.ts | 14 ++ .../aws-lambda-event-sources/lib/sqs.ts | 36 ++++ .../package-lock.json | 5 + .../aws-lambda-event-sources/package.json | 66 ++++++ .../test/integ.s3.expected.json | 188 ++++++++++++++++ .../aws-lambda-event-sources/test/integ.s3.ts | 22 ++ .../test/integ.sns.expected.json | 85 ++++++++ .../test/integ.sns.ts | 19 ++ .../test/integ.sqs.expected.json | 107 ++++++++++ .../test/integ.sqs.ts | 21 ++ .../test/test-function.ts | 18 ++ .../aws-lambda-event-sources/test/test.s3.ts | 85 ++++++++ .../aws-lambda-event-sources/test/test.sns.ts | 47 ++++ .../aws-lambda-event-sources/test/test.sqs.ts | 117 ++++++++++ .../aws-lambda/lib/event-source-mapping.ts | 87 ++++++++ .../@aws-cdk/aws-lambda/lib/event-source.ts | 13 ++ packages/@aws-cdk/aws-lambda/lib/index.ts | 2 + .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 14 ++ .../@aws-cdk/aws-lambda/test/test.lambda.ts | 25 +++ 26 files changed, 1345 insertions(+) create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/.gitignore create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/.npmignore create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/LICENSE create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/NOTICE create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/README.md create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/package-lock.json create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/package.json create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.expected.json create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/test-function.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/test.s3.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/test.sqs.ts create mode 100644 packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts create mode 100644 packages/@aws-cdk/aws-lambda/lib/event-source.ts diff --git a/packages/@aws-cdk/aws-lambda-event-sources/.gitignore b/packages/@aws-cdk/aws-lambda-event-sources/.gitignore new file mode 100644 index 0000000000000..db2eff8d0f036 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/.gitignore @@ -0,0 +1,17 @@ +*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +tsconfig.json +tslint.json + +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/.npmignore b/packages/@aws-cdk/aws-lambda-event-sources/.npmignore new file mode 100644 index 0000000000000..b757d55c46996 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/.npmignore @@ -0,0 +1,16 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/LICENSE b/packages/@aws-cdk/aws-lambda-event-sources/LICENSE new file mode 100644 index 0000000000000..1739faaebb745 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-lambda-event-sources/NOTICE b/packages/@aws-cdk/aws-lambda-event-sources/NOTICE new file mode 100644 index 0000000000000..95fd48569c743 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-lambda-event-sources/README.md b/packages/@aws-cdk/aws-lambda-event-sources/README.md new file mode 100644 index 0000000000000..7cd00be778cd7 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/README.md @@ -0,0 +1,102 @@ +## AWS Lambda Event Sources + +This module includes classes that allow using various AWS services as event +sources for AWS Lambda via the high-level `lambda.addEventSource(source)` API. + +NOTE: In most cases, it is also possible to use the resource APIs to invoke an +AWS Lambda function. This library provides a uniform API for all Lambda event +sources regardless of the underlying mechanism they use. + +### SQS + +Amazon Simple Queue Service (Amazon SQS) allows you to build asynchronous +workflows. For more information about Amazon SQS, see Amazon Simple Queue +Service. You can configure AWS Lambda to poll for these messages as they arrive +and then pass the event to a Lambda function invocation. To view a sample event, +see [Amazon SQS Event](https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-sqs). + +To set up Amazon Simple Queue Service as an event source for AWS Lambda, you +first create or update an Amazon SQS queue and select custom values for the +queue parameters. The following parameters will impact Amazon SQS's polling +behavior: + +* __visibilityTimeoutSec__: May impact the period between retries. +* __receiveMessageWaitTimeSec__: Will determine [long + poll](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-long-polling.html) + duration. The default value is 20 seconds. + +```ts +import { SqsEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +const queue = new sqs.Queue(this, 'MyQueue', { + visibilityTimeoutSec: 30 // default, + receiveMessageWaitTimeSec: 20 // default +}); + +lambda.addEventSource(new SqsEventSource(queue, { + batchSize: 10 // default +}); +``` + +### S3 + +You can write Lambda functions to process S3 bucket events, such as the +object-created or object-deleted events. For example, when a user uploads a +photo to a bucket, you might want Amazon S3 to invoke your Lambda function so +that it reads the image and creates a thumbnail for the photo. + +You can use the bucket notification configuration feature in Amazon S3 to +configure the event source mapping, identifying the bucket events that you want +Amazon S3 to publish and which Lambda function to invoke. + +```ts +import { S3EventSource } from '@aws-cdk/aws-lambda-event-sources'; + +const bucket = new s3.Bucket(...); + +lambda.addEventSource(new S3EventSource(bucket, { + events: [ s3.EventType.ObjectCreated, s3.EventType.ObjectDeleted ], + filters: [ { prefix: 'subdir/' } ] // optional +})); +``` + +### SNS + +You can write Lambda functions to process Amazon Simple Notification Service +notifications. When a message is published to an Amazon SNS topic, the service +can invoke your Lambda function by passing the message payload as a parameter. +Your Lambda function code can then process the event, for example publish the +message to other Amazon SNS topics, or send the message to other AWS services. + +This also enables you to trigger a Lambda function in response to Amazon +CloudWatch alarms and other AWS services that use Amazon SNS. + +For an example event, see [Appendix: Message and JSON +Formats](https://docs.aws.amazon.com/sns/latest/dg/json-formats.html) and +[Amazon SNS Sample +Event](https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-sns). +For an example use case, see [Using AWS Lambda with Amazon SNS from Different +Accounts](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html). + +```ts +import { SnsEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +const topic = new sns.Topic(...); + +lambda.addEventSource(new SnsEventSource(topic)); +``` + +When a user calls the SNS Publish API on a topic that your Lambda function is +subscribed to, Amazon SNS will call Lambda to invoke your function +asynchronously. Lambda will then return a delivery status. If there was an error +calling Lambda, Amazon SNS will retry invoking the Lambda function up to three +times. After three tries, if Amazon SNS still could not successfully invoke the +Lambda function, then Amazon SNS will send a delivery status failure message to +CloudWatch. + +## Roadmap + +Eventually, this module will support all the event sources described under +[Supported Event +Sources](https://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html) +in the AWS Lambda Developer Guide. diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts new file mode 100644 index 0000000000000..7939a4539f17f --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts @@ -0,0 +1,3 @@ +export * from './sqs'; +export * from './s3'; +export * from './sns'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts new file mode 100644 index 0000000000000..5682f91fc9bf9 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts @@ -0,0 +1,33 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import s3 = require('@aws-cdk/aws-s3'); + +export interface S3EventSourceProps { + /** + * The s3 event types that will trigger the notification. + */ + events: s3.EventType[]; + + /** + * S3 object key filter rules to determine which objects trigger this event. + * Each filter must include a `prefix` and/or `suffix` that will be matched + * against the s3 object key. Refer to the S3 Developer Guide for details + * about allowed filter rules. + */ + filters?: s3.NotificationKeyFilter[]; +} + +/** + * Use S3 bucket notifications as an event source for AWS Lambda. + */ +export class S3EventSource implements lambda.IEventSource { + constructor(readonly bucket: s3.Bucket, private readonly props: S3EventSourceProps) { + + } + + public bind(target: lambda.FunctionRef) { + const filters = this.props.filters || []; + for (const event of this.props.events) { + this.bucket.onEvent(event, target, ...filters); + } + } +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts new file mode 100644 index 0000000000000..093bfd8a37d76 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts @@ -0,0 +1,14 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import sns = require('@aws-cdk/aws-sns'); + +/** + * Use an Amazon SNS topic as an event source for AWS Lambda. + */ +export class SnsEventSource implements lambda.IEventSource { + constructor(readonly topic: sns.TopicRef) { + } + + public bind(target: lambda.FunctionRef) { + this.topic.subscribeLambda(target); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts new file mode 100644 index 0000000000000..3cb3273ae6146 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts @@ -0,0 +1,36 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import sqs = require('@aws-cdk/aws-sqs'); + +export interface SqsEventSourceProps { + /** + * The largest number of records that AWS Lambda will retrieve from your event + * source at the time of invoking your function. Your function receives an + * event with all the retrieved records. + * + * Valid Range: Minimum value of 1. Maximum value of 10. + * + * @default 10 + */ + batchSize?: number; +} + +/** + * Use an Amazon SQS queue as an event source for AWS Lambda. + */ +export class SqsEventSource implements lambda.IEventSource { + constructor(readonly queue: sqs.QueueRef, private readonly props: SqsEventSourceProps = { }) { + if (this.props.batchSize !== undefined && (this.props.batchSize < 1 || this.props.batchSize > 10)) { + throw new Error(`Maximum batch size must be between 1 and 10 inclusive (given ${this.props.batchSize})`); + } + } + + public bind(target: lambda.FunctionRef) { + new lambda.EventSourceMapping(target, `SqsEventSource:${this.queue.uniqueId}`, { + target, + batchSize: this.props.batchSize, + eventSourceArn: this.queue.queueArn, + }); + + this.queue.grantConsumeMessages(target.role); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package-lock.json b/packages/@aws-cdk/aws-lambda-event-sources/package-lock.json new file mode 100644 index 0000000000000..e361a3e47886b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "@aws-cdk/aws-lambda", + "version": "0.9.0", + "lockfileVersion": 1 +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json new file mode 100644 index 0000000000000..86dd642eeac80 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -0,0 +1,66 @@ +{ + "name": "@aws-cdk/aws-lambda-event-sources", + "version": "0.14.1", + "description": "Event sources for AWS Lambda", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.lambda.eventsources", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "lambda-event-sources" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.Lambda.EventSources", + "packageId": "Amazon.CDK.AWS.Lambda.EventSources", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk" + }, + "sphinx": {} + } + }, + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-cdk.git" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package" + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "lambda" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "^0.14.1", + "cdk-build-tools": "^0.14.1", + "cdk-integ-tools": "^0.14.1", + "pkglint": "^0.14.1" + }, + "dependencies": { + "@aws-cdk/aws-iam": "^0.14.1", + "@aws-cdk/aws-lambda": "^0.14.1", + "@aws-cdk/aws-sqs": "^0.14.1", + "@aws-cdk/aws-s3": "^0.14.1", + "@aws-cdk/aws-sns": "^0.14.1", + "@aws-cdk/cdk": "^0.14.1" + }, + "homepage": "https://github.com/awslabs/aws-cdk" +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json new file mode 100644 index 0000000000000..5a3cf94b6fb8b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json @@ -0,0 +1,188 @@ +{ + "Resources": { + "FServiceRole3AC82EE1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "FC4345940": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function handler(event) {\n console.log('event:', JSON.stringify(event, undefined, 2));\n return { event };\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "FServiceRole3AC82EE1", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "FServiceRole3AC82EE1" + ] + }, + "FAllowBucketNotificationsFromlambdaeventsources3B101C8CFBA8EBB2AA": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "FC4345940" + }, + "Principal": "s3.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "SourceArn": { + "Fn::GetAtt": [ + "B08E7C7AF", + "Arn" + ] + } + } + }, + "B08E7C7AF": { + "Type": "AWS::S3::Bucket" + }, + "BNotificationsEB8DA980": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "B08E7C7AF" + }, + "NotificationConfiguration": { + "LambdaFunctionConfigurations": [ + { + "Events": [ + "s3:ObjectCreated:*" + ], + "Filter": { + "Key": { + "FilterRules": [ + { + "Name": "prefix", + "Value": "subdir/" + } + ] + } + }, + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "FC4345940", + "Arn" + ] + } + } + ] + } + }, + "DependsOn": [ + "FAllowBucketNotificationsFromlambdaeventsources3B101C8CFBA8EBB2AA" + ] + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "exports.handler = (event, context) => {\n const s3 = new (require('aws-sdk').S3)();\n const https = require(\"https\");\n const url = require(\"url\");\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse(\"FAILED\", err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse(\"SUCCESS\");\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // tslint:disable-next-line:max-line-length\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || \"See the details in CloudWatch Log Stream: \" + context.logStreamName,\n PhysicalResourceId: context.logStreamName,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: \"PUT\",\n headers: {\n \"content-type\": \"\",\n \"content-length\": responseBody.length\n }\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on(\"error\", (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Timeout": 300 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.ts new file mode 100644 index 0000000000000..3bf3f36e4aaa2 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.ts @@ -0,0 +1,22 @@ +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import { S3EventSource } from '../lib'; +import { TestFunction } from './test-function'; + +class S3EventSourceTest extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + const fn = new TestFunction(this, 'F'); + const bucket = new s3.Bucket(this, 'B'); + + fn.addEventSource(new S3EventSource(bucket, { + events: [ s3.EventType.ObjectCreated ], + filters: [ { prefix: 'subdir/' } ] + })); + } +} + +const app = new cdk.App(); +new S3EventSourceTest(app, 'lambda-event-source-s3'); +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json new file mode 100644 index 0000000000000..94a40b16aada1 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json @@ -0,0 +1,85 @@ +{ + "Resources": { + "FServiceRole3AC82EE1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "FC4345940": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function handler(event) {\n console.log('event:', JSON.stringify(event, undefined, 2));\n return { event };\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "FServiceRole3AC82EE1", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "FServiceRole3AC82EE1" + ] + }, + "FT1706F790": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "FC4345940" + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "TD925BC7E" + } + } + }, + "TD925BC7E": { + "Type": "AWS::SNS::Topic" + }, + "TFSubscription47A24A95": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "FC4345940", + "Arn" + ] + }, + "Protocol": "lambda", + "TopicArn": { + "Ref": "TD925BC7E" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.ts new file mode 100644 index 0000000000000..790b4bcc265e8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.ts @@ -0,0 +1,19 @@ +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import { SnsEventSource } from '../lib'; +import { TestFunction } from './test-function'; + +class SqsEventSourceTest extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + const fn = new TestFunction(this, 'F'); + const topic = new sns.Topic(this, 'T'); + + fn.addEventSource(new SnsEventSource(topic)); + } +} + +const app = new cdk.App(); +new SqsEventSourceTest(app, 'lambda-event-source-sns'); +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.expected.json new file mode 100644 index 0000000000000..e590deace266b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.expected.json @@ -0,0 +1,107 @@ +{ + "Resources": { + "FServiceRole3AC82EE1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "FServiceRoleDefaultPolicy17A19BFA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:ChangeMessageVisibilityBatch", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:DeleteMessageBatch", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Q63C6E3AB", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FServiceRoleDefaultPolicy17A19BFA", + "Roles": [ + { + "Ref": "FServiceRole3AC82EE1" + } + ] + } + }, + "FC4345940": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function handler(event) {\n console.log('event:', JSON.stringify(event, undefined, 2));\n return { event };\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "FServiceRole3AC82EE1", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "FServiceRole3AC82EE1", + "FServiceRoleDefaultPolicy17A19BFA" + ] + }, + "FSqsEventSourcelambdaeventsourcesqsQ67DE9201754EC819": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "EventSourceArn": { + "Fn::GetAtt": [ + "Q63C6E3AB", + "Arn" + ] + }, + "FunctionName": { + "Ref": "FC4345940" + }, + "BatchSize": 5 + } + }, + "Q63C6E3AB": { + "Type": "AWS::SQS::Queue" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.ts new file mode 100644 index 0000000000000..77ab3556b5d2a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sqs.ts @@ -0,0 +1,21 @@ +import sqs = require('@aws-cdk/aws-sqs'); +import cdk = require('@aws-cdk/cdk'); +import { SqsEventSource } from '../lib'; +import { TestFunction } from './test-function'; + +class SqsEventSourceTest extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + const fn = new TestFunction(this, 'F'); + const queue = new sqs.Queue(this, 'Q'); + + fn.addEventSource(new SqsEventSource(queue, { + batchSize: 5 + })); + } +} + +const app = new cdk.App(); +new SqsEventSourceTest(app, 'lambda-event-source-sqs'); +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test-function.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test-function.ts new file mode 100644 index 0000000000000..962f2929bc7e6 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test-function.ts @@ -0,0 +1,18 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); + +export class TestFunction extends lambda.Function { + constructor(parent: cdk.Construct, id: string) { + super(parent, id, { + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${handler.toString()}`), + runtime: lambda.Runtime.NodeJS810 + }); + } +} + +// tslint:disable:no-console +async function handler(event: any) { + console.log('event:', JSON.stringify(event, undefined, 2)); + return { event }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.s3.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.s3.ts new file mode 100644 index 0000000000000..ef0a192ac618b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.s3.ts @@ -0,0 +1,85 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import sources = require('../lib'); +import { TestFunction } from './test-function'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'sufficiently complex example'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const bucket = new s3.Bucket(stack, 'B'); + + // WHEN + fn.addEventSource(new sources.S3EventSource(bucket, { + events: [ s3.EventType.ObjectCreated, s3.EventType.ObjectRemoved ], + filters: [ + { prefix: 'prefix/' }, + { suffix: '.png' } + ] + })); + + // THEN + expect(stack).to(haveResource('Custom::S3BucketNotifications', { + "NotificationConfiguration": { + "LambdaFunctionConfigurations": [ + { + "Events": [ + "s3:ObjectCreated:*" + ], + "Filter": { + "Key": { + "FilterRules": [ + { + "Name": "prefix", + "Value": "prefix/" + }, + { + "Name": "suffix", + "Value": ".png" + } + ] + } + }, + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "Fn9270CBC0", + "Arn" + ] + } + }, + { + "Events": [ + "s3:ObjectRemoved:*" + ], + "Filter": { + "Key": { + "FilterRules": [ + { + "Name": "prefix", + "Value": "prefix/" + }, + { + "Name": "suffix", + "Value": ".png" + } + ] + } + }, + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "Fn9270CBC0", + "Arn" + ] + } + } + ] + } + })); + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts new file mode 100644 index 0000000000000..bb9f5c1f11a4d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts @@ -0,0 +1,47 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import sources = require('../lib'); +import { TestFunction } from './test-function'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'sufficiently complex example'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const topic = new sns.Topic(stack, 'T'); + + // WHEN + fn.addEventSource(new sources.SnsEventSource(topic)); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Fn9270CBC0" + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "TD925BC7E" + } + })); + + expect(stack).to(haveResource('AWS::SNS::Subscription', { + "Endpoint": { + "Fn::GetAtt": [ + "Fn9270CBC0", + "Arn" + ] + }, + "Protocol": "lambda", + "TopicArn": { + "Ref": "TD925BC7E" + } + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sqs.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.sqs.ts new file mode 100644 index 0000000000000..2fffbf66ea9e0 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.sqs.ts @@ -0,0 +1,117 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import sqs = require('@aws-cdk/aws-sqs'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import sources = require('../lib'); +import { TestFunction } from './test-function'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'defaults'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const q = new sqs.Queue(stack, 'Q'); + + // WHEN + fn.addEventSource(new sources.SqsEventSource(q)); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:ChangeMessageVisibilityBatch", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:DeleteMessageBatch", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Q63C6E3AB", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + } + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + "EventSourceArn": { + "Fn::GetAtt": [ + "Q63C6E3AB", + "Arn" + ] + }, + "FunctionName": { + "Ref": "Fn9270CBC0" + } + })); + + test.done(); + }, + + 'specific batch size'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const q = new sqs.Queue(stack, 'Q'); + + // WHEN + fn.addEventSource(new sources.SqsEventSource(q, { + batchSize: 5 + })); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + "EventSourceArn": { + "Fn::GetAtt": [ + "Q63C6E3AB", + "Arn" + ] + }, + "FunctionName": { + "Ref": "Fn9270CBC0" + }, + "BatchSize": 5 + })); + + test.done(); + }, + + 'fails if batch size is < 1'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const q = new sqs.Queue(stack, 'Q'); + + // WHEN/THEN + test.throws(() => fn.addEventSource(new sources.SqsEventSource(q, { + batchSize: 0 + })), /Maximum batch size must be between 1 and 10 inclusive \(given 0\)/); + + test.done(); + }, + + 'fails if batch size is > 10'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const q = new sqs.Queue(stack, 'Q'); + + // WHEN/THEN + test.throws(() => fn.addEventSource(new sources.SqsEventSource(q, { + batchSize: 11 + })), /Maximum batch size must be between 1 and 10 inclusive \(given 11\)/); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts new file mode 100644 index 0000000000000..1f6326362dcb7 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts @@ -0,0 +1,87 @@ +import cdk = require('@aws-cdk/cdk'); +import { FunctionRef } from './lambda-ref'; +import { cloudformation } from './lambda.generated'; + +export interface EventSourceMappingProps { + /** + * The Amazon Resource Name (ARN) of the event source. Any record added to + * this stream can invoke the Lambda function. + */ + eventSourceArn: string; + + /** + * The target AWS Lambda function. + */ + target: FunctionRef; + + /** + * The largest number of records that AWS Lambda will retrieve from your event + * source at the time of invoking your function. Your function receives an + * event with all the retrieved records. + * + * Valid Range: Minimum value of 1. Maximum value of 10000. + * + * @default The default for Amazon Kinesis and Amazon DynamoDB is 100 records. + * Both the default and maximum for Amazon SQS are 10 messages. + */ + batchSize?: number; + + /** + * Set to false to disable the event source upon creation. + * + * @default true + */ + enabled?: boolean; + + /** + * The position in the DynamoDB or Kinesis stream where AWS Lambda should + * start reading. + * + * @see https://docs.aws.amazon.com/kinesis/latest/APIReference/API_GetShardIterator.html#Kinesis-GetShardIterator-request-ShardIteratorType + */ + startingPosition?: StartingPosition +} + +/** + * Defines a Lambda EventSourceMapping resource. + * + * Usually, you won't need to define the mapping yourself. This will usually be done by + * event sources. For example, to add an SQS event source to a function: + * + * import { SqsEventSource } from '@aws-cdk/aws-lambda-event-sources'; + * lambda.addEventSource(new SqsEventSource(sqs)); + * + * The `SqsEventSource` class will automatically create the mapping, and will also + * modify the Lambda's execution role so it can consume messages from the queue. + */ +export class EventSourceMapping extends cdk.Construct { + constructor(parent: cdk.Construct, id: string, props: EventSourceMappingProps) { + super(parent, id); + + new cloudformation.EventSourceMappingResource(this, 'Resource', { + batchSize: props.batchSize, + enabled: props.enabled, + eventSourceArn: props.eventSourceArn, + functionName: props.target.functionName, + startingPosition: props.startingPosition, + }); + } +} + +/** + * The position in the DynamoDB or Kinesis stream where AWS Lambda should start + * reading. + */ +export enum StartingPosition { + /** + * Start reading at the last untrimmed record in the shard in the system, + * which is the oldest data record in the shard. + */ + TrimHorizon = 'TRIM_HORIZON', + + /** + * Start reading just after the most recent record in the shard, so that you + * always read the most recent data in the shard + */ + Latest = 'LATEST', +} diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source.ts b/packages/@aws-cdk/aws-lambda/lib/event-source.ts new file mode 100644 index 0000000000000..76aa56311e05e --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/event-source.ts @@ -0,0 +1,13 @@ +import { FunctionRef } from './lambda-ref'; + +/** + * An abstract class which represents an AWS Lambda event source. + */ +export interface IEventSource { + /** + * Called by `lambda.addEventSource` to allow the event source to bind to this + * function. + * @param target That lambda function to bind to. + */ + bind(target: FunctionRef): void; +} diff --git a/packages/@aws-cdk/aws-lambda/lib/index.ts b/packages/@aws-cdk/aws-lambda/lib/index.ts index 2874f05f53a90..1304dd96c2618 100644 --- a/packages/@aws-cdk/aws-lambda/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/index.ts @@ -7,6 +7,8 @@ export * from './runtime'; export * from './code'; export * from './lambda-version'; export * from './singleton-lambda'; +export * from './event-source'; +export * from './event-source-mapping'; // AWS::Lambda CloudFormation Resources: export * from './lambda.generated'; diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 11c5b85ba525b..8b81a01d9cc32 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -7,6 +7,7 @@ import logs = require('@aws-cdk/aws-logs'); import s3n = require('@aws-cdk/aws-s3-notifications'); import stepfunctions = require('@aws-cdk/aws-stepfunctions'); import cdk = require('@aws-cdk/cdk'); +import { IEventSource } from './event-source'; import { cloudformation } from './lambda.generated'; import { Permission } from './permission'; import { CommonPipelineInvokeActionProps, PipelineInvokeAction } from './pipeline-action'; @@ -377,6 +378,19 @@ export abstract class FunctionRef extends cdk.Construct }; } + /** + * Adds an event source to this function. + * + * Any type that implements the IEventSource interface can be used here. For + * example, you can call this with an SQS queue: `lambda.addEventSource(queue)`. + * + * @param source The event source + * @param options Event source mapping options (e.g. batch size, enabled, etc) + */ + public addEventSource(source: IEventSource) { + source.bind(this); + } + private parsePermissionPrincipal(principal?: iam.PolicyPrincipal) { if (!principal) { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 73a919afacfb8..fe5fcf1f3f138 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1060,6 +1060,31 @@ export = { test.done(); }, + + 'addEventSource calls bind'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.inline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + }); + + let bindTarget; + + class EventSourceMock implements lambda.IEventSource { + public bind(target: lambda.FunctionRef) { + bindTarget = target; + } + } + + // WHEN + fn.addEventSource(new EventSourceMock()); + + // THEN + test.same(bindTarget, fn); + test.done(); + } }; function newTestLambda(parent: cdk.Construct) {