Skip to content

Commit

Permalink
Initial work on bucket notifications
Browse files Browse the repository at this point in the history
Remaining work:

- [ ] Define a custom resource to break the cycle
- [ ] Add `IBucketNotificationTarget` on SQS
  • Loading branch information
Elad Ben-Israel committed Jun 30, 2018
1 parent ae40183 commit 625aef4
Show file tree
Hide file tree
Showing 14 changed files with 572 additions and 8 deletions.
10 changes: 9 additions & 1 deletion packages/@aws-cdk/lambda/lib/lambda-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AccountPrincipal, Arn, Construct, FnSelect, FnSplit, PolicyPrincipal,
import { EventRuleTarget, IEventRuleTarget } from '@aws-cdk/events';
import { Role } from '@aws-cdk/iam';
import { lambda } from '@aws-cdk/resources';
import { BucketNotificationTarget, BucketNotificationTargetType, IBucketNotificationTarget } from '@aws-cdk/s3';
import { LambdaPermission } from './permission';

/**
Expand All @@ -22,7 +23,7 @@ export interface LambdaRefProps {
role?: Role;
}

export abstract class LambdaRef extends Construct implements IEventRuleTarget {
export abstract class LambdaRef extends Construct implements IEventRuleTarget, IBucketNotificationTarget {
/**
* Creates a Lambda function object which represents a function not defined
* within this stack.
Expand Down Expand Up @@ -135,6 +136,13 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
arn: this.functionArn,
};
}

public get bucketNotificationTarget(): BucketNotificationTarget {
return {
type: BucketNotificationTargetType.Lambda,
arn: this.functionArn
};
}
}

class LambdaRefImport extends LambdaRef {
Expand Down
96 changes: 93 additions & 3 deletions packages/@aws-cdk/s3/lib/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IIdentityResource } from '@aws-cdk/iam';
import * as kms from '@aws-cdk/kms';
import { s3 } from '@aws-cdk/resources';
import { BucketPolicy } from './bucket-policy';
import { EventType, IBucketNotificationTarget, BucketNotificationTargetType } from './notifications';
import * as perms from './perms';
import { LifecycleRule } from './rule';
import { parseBucketArn, parseBucketName, validateBucketName } from './util';
Expand Down Expand Up @@ -257,6 +258,7 @@ export class Bucket extends BucketRef {
protected autoCreatePolicy = true;
private readonly lifecycleRules: LifecycleRule[] = [];
private readonly versioned?: boolean;
private readonly notifications = new Array<BucketNotification>();

constructor(parent: Construct, name: string, props: BucketProps = {}) {
super(parent, name);
Expand All @@ -269,7 +271,8 @@ export class Bucket extends BucketRef {
bucketName: props && props.bucketName,
bucketEncryption,
versioningConfiguration: props.versioned ? { status: 'Enabled' } : undefined,
lifecycleConfiguration: new Token(() => this.parseLifecycleConfiguration()),
lifecycleConfiguration: new Token(() => this.renderLifecycleConfiguration()),
notificationConfiguration: new Token(() => this.renderNotificationConfiguration())
});

applyRemovalPolicy(resource, props.removalPolicy);
Expand Down Expand Up @@ -301,6 +304,27 @@ export class Bucket extends BucketRef {
this.lifecycleRules.push(rule);
}

/**
* Adds a bucket notification event target.
* @param event The event to trigger the notification
* @param target The target (Lambda, SNS Topic or SQS Queue)
*
* @param filterRules S3 filter rules to determine which objects trigger
* this event. Rules must include either a prefix asterisk ("*foo/bar") or
* suffix asterisk ("foo/bar*") to indicate if this is a prefix or a suffix
* rule.
*
* @example
*
* bucket.onEvent(EventType.OnObjectCreated, myLambda, 'home/myusername/*')
*
* @see
* https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
*/
public onEvent(event: EventType, target: IBucketNotificationTarget, ...filterRules: string[]) {
this.notifications.push({ event, target, filterRules });
}

/**
* Set up key properties and return the Bucket encryption property from the
* user's configuration.
Expand Down Expand Up @@ -353,10 +377,10 @@ export class Bucket extends BucketRef {
}

/**
* Parse the lifecycle configuration out of the uucket props
* Render the lifecycle configuration based on bucket props
* @param props Par
*/
private parseLifecycleConfiguration(): s3.BucketResource.LifecycleConfigurationProperty | undefined {
private renderLifecycleConfiguration(): s3.BucketResource.LifecycleConfigurationProperty | undefined {
if (!this.lifecycleRules || this.lifecycleRules.length === 0) {
return undefined;
}
Expand Down Expand Up @@ -394,6 +418,66 @@ export class Bucket extends BucketRef {
}));
}
}

private renderNotificationConfiguration(): s3.BucketResource.NotificationConfigurationProperty | undefined {
if (this.notifications.length === 0) {
return undefined;
}

const lambda = new Array<s3.BucketResource.LambdaConfigurationProperty>();
const queue = new Array<s3.BucketResource.QueueConfigurationProperty>();
const topic = new Array<s3.BucketResource.TopicConfigurationProperty>();

for (const notification of this.notifications) {
const event = notification .event;
const filter = renderFilter(notification.filterRules);
const type = notification.target.bucketNotificationTarget.type;
const arn = notification.target.bucketNotificationTarget.arn;
switch (type) {
case BucketNotificationTargetType.Lambda:
lambda.push({ event, filter, function: arn });
break;
case BucketNotificationTargetType.Topic:
topic.push({ event, filter, topic: arn });
break;
case BucketNotificationTargetType.Queue:
queue.push({ event, filter, queue: arn });
break;
default:
throw new Error('Unsupported notification target type:' + notification.target.bucketNotificationTargetType);
}
}

return {
lambdaConfigurations: lambda.length > 0 ? lambda : undefined,
queueConfigurations: queue.length > 0 ? queue : undefined,
topicConfigurations: topic.length > 0 ? topic : undefined
};

function renderFilter(rules?: string[]): s3.BucketResource.NotificationFilterProperty | undefined {
if (!rules || rules.length === 0) {
return undefined;
}

const renderedRules = new Array<s3.BucketResource.FilterRuleProperty>();

for (const rule of rules) {
if (rule.startsWith('*')) {
renderedRules.push({ name: 'suffix', value: rule.substr(1) });
} else if (rule.endsWith('*')) {
renderedRules.push({ name: 'prefix', value: rule.substr(0, rule.length - 1) });
} else {
throw new Error('Rule must either have a "*" prefix or suffix to indicate the rule type: ' + rule);
}
}

return {
s3Key: {
rules: renderedRules
}
};
}
}
}

/**
Expand Down Expand Up @@ -441,3 +525,9 @@ class ImportedBucketRef extends BucketRef {
this.policy = undefined;
}
}

interface BucketNotification {
event: EventType;
target: IBucketNotificationTarget;
filterRules?: string[];
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/s3/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './bucket';
export * from './bucket-policy';
export * from './rule';
export * from './rule';
export * from './notifications';
143 changes: 143 additions & 0 deletions packages/@aws-cdk/s3/lib/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Arn } from "@aws-cdk/core";
import { s3 } from '@aws-cdk/resources';

export enum EventType {
/**
* Amazon S3 APIs such as PUT, POST, and COPY can create an object. Using
* these event types, you can enable notification when an object is created
* using a specific API, or you can use the s3:ObjectCreated:* event type to
* request notification regardless of the API that was used to create an
* object.
*/
ObjectCreated = 's3:ObjectCreated:*',

/**
* Amazon S3 APIs such as PUT, POST, and COPY can create an object. Using
* these event types, you can enable notification when an object is created
* using a specific API, or you can use the s3:ObjectCreated:* event type to
* request notification regardless of the API that was used to create an
* object.
*/
ObjectCreatedPut = 's3:ObjectCreated:Put',

/**
* Amazon S3 APIs such as PUT, POST, and COPY can create an object. Using
* these event types, you can enable notification when an object is created
* using a specific API, or you can use the s3:ObjectCreated:* event type to
* request notification regardless of the API that was used to create an
* object.
*/
ObjectCreatedPost = 's3:ObjectCreated:Post',

/**
* Amazon S3 APIs such as PUT, POST, and COPY can create an object. Using
* these event types, you can enable notification when an object is created
* using a specific API, or you can use the s3:ObjectCreated:* event type to
* request notification regardless of the API that was used to create an
* object.
*/
ObjectCreatedCopy = 's3:ObjectCreated:Copy',

/**
* Amazon S3 APIs such as PUT, POST, and COPY can create an object. Using
* these event types, you can enable notification when an object is created
* using a specific API, or you can use the s3:ObjectCreated:* event type to
* request notification regardless of the API that was used to create an
* object.
*/
ObjectCreatedCompleteMultipartUpload = 's3:ObjectCreated:CompleteMultipartUpload',

/**
* By using the ObjectRemoved event types, you can enable notification when
* an object or a batch of objects is removed from a bucket.
*
* You can request notification when an object is deleted or a versioned
* object is permanently deleted by using the s3:ObjectRemoved:Delete event
* type. Or you can request notification when a delete marker is created for
* a versioned object by using s3:ObjectRemoved:DeleteMarkerCreated. For
* information about deleting versioned objects, see Deleting Object
* Versions. You can also use a wildcard s3:ObjectRemoved:* to request
* notification anytime an object is deleted.
*
* You will not receive event notifications from automatic deletes from
* lifecycle policies or from failed operations.
*/
ObjectRemoved = 's3:ObjectRemoved:*',

/**
* By using the ObjectRemoved event types, you can enable notification when
* an object or a batch of objects is removed from a bucket.
*
* You can request notification when an object is deleted or a versioned
* object is permanently deleted by using the s3:ObjectRemoved:Delete event
* type. Or you can request notification when a delete marker is created for
* a versioned object by using s3:ObjectRemoved:DeleteMarkerCreated. For
* information about deleting versioned objects, see Deleting Object
* Versions. You can also use a wildcard s3:ObjectRemoved:* to request
* notification anytime an object is deleted.
*
* You will not receive event notifications from automatic deletes from
* lifecycle policies or from failed operations.
*/
ObjectRemovedDelete = 's3:ObjectRemoved:Delete',

/**
* By using the ObjectRemoved event types, you can enable notification when
* an object or a batch of objects is removed from a bucket.
*
* You can request notification when an object is deleted or a versioned
* object is permanently deleted by using the s3:ObjectRemoved:Delete event
* type. Or you can request notification when a delete marker is created for
* a versioned object by using s3:ObjectRemoved:DeleteMarkerCreated. For
* information about deleting versioned objects, see Deleting Object
* Versions. You can also use a wildcard s3:ObjectRemoved:* to request
* notification anytime an object is deleted.
*
* You will not receive event notifications from automatic deletes from
* lifecycle policies or from failed operations.
*/
ObjectRemovedDeleteMarkerCreated = 's3:ObjectRemoved:DeleteMarkerCreated',

/**
* You can use this event type to request Amazon S3 to send a notification
* message when Amazon S3 detects that an object of the RRS storage class is
* lost.
*/
ReducedRedundancyLostObject = 's3:ReducedRedundancyLostObject',
}

export interface BucketNotificationProps {
/**
* The S3 bucket event.
*/
event: EventType;

/**
* The filtering rule that determine which objects trigger the event. For
* example, you can create a filter so that only image files with a .jpg
* extension trigger the event when they are added to the S3 bucket.
*
* @default All objects will trigger the event
*/
s3KeyFilter?: string;

/**
* The target of the notification.
*/
target: IBucketNotificationTarget;
}

export interface IBucketNotificationTarget {
bucketNotificationTarget(bucketArn: s3.BucketArn): BucketNotificationTarget;
}

export interface BucketNotificationTarget {
readonly type: BucketNotificationTargetType;
readonly arn: Arn;
}

export enum BucketNotificationTargetType {
Lambda,
Queue,
Topic
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/s3/test/integ.bucket.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@
}
}
}
}
}
36 changes: 36 additions & 0 deletions packages/@aws-cdk/s3/test/integ.notifications.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"Resources": {
"TestBucket560B80BC": {
"Type": "AWS::S3::Bucket",
"Properties": {
"NotificationConfiguration": {
"TopicConfigurations": [
{
"Event": "s3:ObjectCreated:*",
"Filter": {
"S3Key": {
"Rules": [
{
"Name": "prefix",
"Value": "images/"
},
{
"Name": "suffix",
"Value": ".jpg"
}
]
}
},
"Topic": {
"Ref": "Topic"
}
}
]
}
}
},
"Topic": {
"Type": "AWS::SNS::Topic"
}
}
}
21 changes: 21 additions & 0 deletions packages/@aws-cdk/s3/test/integ.notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { App, Resource, Stack } from '@aws-cdk/core';
import { Bucket, EventType, BucketNotificationTargetType } from '../lib';

const app = new App(process.argv);

const stack = new Stack(app, 'aws-cdk-s3-bucket-notifications');

const bucket = new Bucket(stack, 'TestBucket');

const topic = new Resource(stack, 'Topic', {
type: 'AWS::SNS::Topic'
});

const bucketNotificationTarget = {
type: BucketNotificationTargetType.Topic,
arn: topic.ref
};

bucket.onEvent(EventType.ObjectCreated, { bucketNotificationTarget }, 'images/*', '*.jpg');

process.stdout.write(app.run());
Loading

0 comments on commit 625aef4

Please sign in to comment.