diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 87fae1518431a..862cacd238e6a 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -2,6 +2,7 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import logs = require('@aws-cdk/aws-logs'); +import s3n = require('@aws-cdk/aws-s3-notifications'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, FunctionArn } from './lambda.generated'; import { Permission } from './permission'; @@ -23,7 +24,9 @@ export interface FunctionRefProps { role?: iam.Role; } -export abstract class FunctionRef extends cdk.Construct implements events.IEventRuleTarget, logs.ILogSubscriptionDestination { +export abstract class FunctionRef extends cdk.Construct + implements events.IEventRuleTarget, logs.ILogSubscriptionDestination, s3n.IBucketNotificationDestination { + /** * Creates a Lambda function object which represents a function not defined * within this stack. @@ -138,9 +141,9 @@ export abstract class FunctionRef extends cdk.Construct implements events.IEvent /** * Adds a permission to the Lambda resource policy. - * @param name A name for the permission construct + * @param id The id ƒor the permission construct */ - public addPermission(name: string, permission: Permission) { + public addPermission(id: string, permission: Permission) { if (!this.canCreatePermissions) { // FIXME: Report metadata return; @@ -149,7 +152,7 @@ export abstract class FunctionRef extends cdk.Construct implements events.IEvent const principal = this.parsePermissionPrincipal(permission.principal); const action = permission.action || 'lambda:InvokeFunction'; - new cloudformation.PermissionResource(this, name, { + new cloudformation.PermissionResource(this, id, { action, principal, functionName: this.functionName, @@ -261,6 +264,26 @@ export abstract class FunctionRef extends cdk.Construct implements events.IEvent }; } + /** + * Allows this Lambda to be used as a destination for bucket notifications. + * Use `bucket.onEvent(lambda)` to subscribe. + */ + public asBucketNotificationDestination(bucketArn: cdk.Arn, bucketId: string): s3n.BucketNotificationDestinationProps { + const permissionId = `AllowBucketNotificationsFrom${bucketId}`; + if (!this.tryFindChild(permissionId)) { + this.addPermission(permissionId, { + sourceAccount: new cdk.AwsAccountId(), + principal: new cdk.ServicePrincipal('s3.amazonaws.com'), + sourceArn: bucketArn, + }); + } + + return { + type: s3n.BucketNotificationDestinationType.Lambda, + arn: this.functionArn + }; + } + private parsePermissionPrincipal(principal?: cdk.PolicyPrincipal) { if (!principal) { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index ab0f3b247c9e7..5a108e658cdcb 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -59,6 +59,7 @@ "@aws-cdk/aws-iam": "^0.8.1", "@aws-cdk/aws-logs": "^0.8.1", "@aws-cdk/aws-s3": "^0.8.1", + "@aws-cdk/aws-s3-notifications": "^0.8.1", "@aws-cdk/cdk": "^0.8.1", "@aws-cdk/cx-api": "^0.8.1" }, diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.expected.json new file mode 100644 index 0000000000000..edbf24da3f3f2 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.expected.json @@ -0,0 +1,256 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket" + }, + "MyBucketNotifications46AC0CD2": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "MyBucketF68F3FF0" + }, + "NotificationConfiguration": { + "LambdaFunctionConfigurations": [ + { + "Events": [ + "s3:ObjectCreated:*" + ], + "Filter": { + "Key": { + "FilterRules": [ + { + "Name": "suffix", + "Value": ".png" + } + ] + } + }, + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] + } + } + ] + } + } + }, + "MyFunctionServiceRole3C357FF2": { + "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" + ] + ] + } + ] + } + }, + "MyFunction3BAA72D1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function handler(event, _context, callback) {\n console.log(JSON.stringify(event, undefined, 2));\n return callback(null, event);\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionServiceRole3C357FF2", + "Arn" + ] + }, + "Runtime": "nodejs6.10" + }, + "DependsOn": [ + "MyFunctionServiceRole3C357FF2" + ] + }, + "MyFunctionAllowBucketNotificationsFromlambdabucketnotificationsMyBucket0F0FC402189522F6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyFunction3BAA72D1" + }, + "Principal": "s3.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "SourceArn": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + } + } + }, + "MyFunctionAllowBucketNotificationsFromlambdabucketnotificationsYourBucket307F72F245F2C5AE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyFunction3BAA72D1" + }, + "Principal": "s3.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "SourceArn": { + "Fn::GetAtt": [ + "YourBucketC6A57364", + "Arn" + ] + } + } + }, + "YourBucketC6A57364": { + "Type": "AWS::S3::Bucket" + }, + "YourBucketNotifications8D39901A": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "YourBucketC6A57364" + }, + "NotificationConfiguration": { + "LambdaFunctionConfigurations": [ + { + "Events": [ + "s3:ObjectRemoved:*" + ], + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] + } + } + ] + } + } + }, + "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/test/integ.bucket-notifications.ts b/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.ts new file mode 100644 index 0000000000000..22550911d4e71 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.ts @@ -0,0 +1,28 @@ +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import lambda = require('../lib'); + +const app = new cdk.App(process.argv); + +const stack = new cdk.Stack(app, 'lambda-bucket-notifications'); + +const bucketA = new s3.Bucket(stack, 'MyBucket'); + +const fn = new lambda.Function(stack, 'MyFunction', { + runtime: lambda.Runtime.NodeJS610, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${handler.toString()}`) +}); + +const bucketB = new s3.Bucket(stack, 'YourBucket'); + +bucketA.onObjectCreated(fn, { suffix: '.png' }); +bucketB.onEvent(s3.EventType.ObjectRemoved, fn); + +process.stdout.write(app.run()); + +// tslint:disable:no-console +function handler(event: any, _context: any, callback: any) { + console.log(JSON.stringify(event, undefined, 2)); + return callback(null, event); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts index 309f845c8df5b..3dec683c961a5 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts @@ -53,7 +53,7 @@ export class BucketNotifications extends cdk.Construct { // resolve target. this also provides an opportunity for the target to e.g. update // policies to allow this notification to happen. - const targetProps = target.asBucketNotificationDestination(this.bucket.bucketArn, this.bucket.path); + const targetProps = target.asBucketNotificationDestination(this.bucket.bucketArn, this.bucket.uniqueId); const commonConfig: CommonConfiguration = { Events: [ event ], Filter: renderFilters(filters),