Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lambda: support bucket notifications #561

Merged
merged 4 commits into from
Aug 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
28 changes: 28 additions & 0 deletions packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down