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

Fix: S3 bucket notification dependencies #584

Merged
merged 2 commits into from
Aug 16, 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
7 changes: 6 additions & 1 deletion packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,14 @@ export abstract class FunctionRef extends cdk.Construct
});
}

// if we have a permission resource for this relationship, add it as a dependency
// to the bucket notifications resource, so it will be created first.
const permission = this.tryFindChild(permissionId) as cdk.Resource;

return {
type: s3n.BucketNotificationDestinationType.Lambda,
arn: this.functionArn
arn: this.functionArn,
dependencies: permission ? [ permission ] : undefined
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
}
]
}
}
},
"DependsOn": [
"MyFunctionAllowBucketNotificationsFromlambdabucketnotificationsMyBucket0F0FC402189522F6"
]
},
"MyFunctionServiceRole3C357FF2": {
"Type": "AWS::IAM::Role",
Expand Down Expand Up @@ -170,7 +173,10 @@
}
]
}
}
},
"DependsOn": [
"MyFunctionAllowBucketNotificationsFromlambdabucketnotificationsYourBucket307F72F245F2C5AE"
]
},
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": {
"Type": "AWS::IAM::Role",
Expand Down
10 changes: 8 additions & 2 deletions packages/@aws-cdk/aws-s3-notifications/lib/destination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@ export interface BucketNotificationDestinationProps {
/**
* The notification type.
*/
readonly type: BucketNotificationDestinationType;
type: BucketNotificationDestinationType;

/**
* The ARN of the destination (i.e. Lambda, SNS, SQS).
*/
readonly arn: cdk.Arn;
arn: cdk.Arn;

/**
* Any additional dependencies that should be resolved before the bucket notification
* can be configured (for example, the SNS Topic Policy resource).
*/
dependencies?: cdk.IDependable[]
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class BucketNotifications extends cdk.Construct {
private readonly lambdaNotifications = new Array<LambdaFunctionConfiguration>();
private readonly queueNotifications = new Array<QueueConfiguration>();
private readonly topicNotifications = new Array<TopicConfiguration>();
private customResourceCreated = false;
private resource?: cdk.Resource;
private readonly bucket: Bucket;

constructor(parent: cdk.Construct, id: string, props: NotificationsProps) {
Expand All @@ -49,7 +49,7 @@ export class BucketNotifications extends cdk.Construct {
* @param filters A set of S3 key filters
*/
public addNotification(event: EventType, target: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) {
this.createResourceOnce();
const resource = this.createResourceOnce();

// resolve target. this also provides an opportunity for the target to e.g. update
// policies to allow this notification to happen.
Expand All @@ -59,6 +59,13 @@ export class BucketNotifications extends cdk.Construct {
Filter: renderFilters(filters),
};

// if the target specifies any dependencies, add them to the custom resource.
// for example, the SNS topic policy must be created /before/ the notification resource.
// otherwise, S3 won't be able to confirm the subscription.
if (targetProps.dependencies) {
resource.addDependency(...targetProps.dependencies);
}

// based on the target type, add the the correct configurations array
switch (targetProps.type) {
case BucketNotificationDestinationType.Lambda:
Expand Down Expand Up @@ -92,21 +99,20 @@ export class BucketNotifications extends cdk.Construct {
* there is no notifications resource.
*/
private createResourceOnce() {
if (this.customResourceCreated) {
return;
if (!this.resource) {
const handlerArn = NotificationsResourceHandler.singleton(this);

this.resource = new cdk.Resource(this, 'Resource', {
type: 'Custom::S3BucketNotifications',
properties: {
ServiceToken: handlerArn,
BucketName: this.bucket.bucketName,
NotificationConfiguration: new cdk.Token(() => this.renderNotificationConfiguration())
}
});
}

const handlerArn = NotificationsResourceHandler.singleton(this);
new cdk.Resource(this, 'Resource', {
type: 'Custom::S3BucketNotifications',
properties: {
ServiceToken: handlerArn,
BucketName: this.bucket.bucketName,
NotificationConfiguration: new cdk.Token(() => this.renderNotificationConfiguration())
}
});

this.customResourceCreated = true;
return this.resource;
}
}

Expand Down
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-s3/test/test.notifications.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, haveResource } from '@aws-cdk/assert';
import s3n = require('@aws-cdk/aws-s3-notifications');
import cdk = require('@aws-cdk/cdk');
import { Stack } from '@aws-cdk/cdk';
import { Test } from 'nodeunit';
import s3 = require('../lib');
import { Topic } from './notification-dests';
Expand Down Expand Up @@ -268,6 +269,34 @@ export = {
}
}));

test.done();
},

'a notification destination can specify a set of dependencies that must be resolved before the notifications resource is created'(test: Test) {
const stack = new Stack();

const bucket = new s3.Bucket(stack, 'Bucket');
const dependent = new cdk.Resource(stack, 'Dependent', { type: 'DependOnMe' });
const dest: s3n.IBucketNotificationDestination = {
asBucketNotificationDestination: () => ({
arn: new cdk.Arn('arn'),
type: s3n.BucketNotificationDestinationType.Queue,
dependencies: [ dependent ]
})
};

bucket.onObjectCreated(dest);

test.deepEqual(stack.toCloudFormation().Resources.BucketNotifications8F2E257D, {
Type: 'Custom::S3BucketNotifications',
Properties: {
ServiceToken: { 'Fn::GetAtt': [ 'BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691', 'Arn' ] },
BucketName: { Ref: 'Bucket83908E77' },
NotificationConfiguration: { QueueConfigurations: [ { Events: [ 's3:ObjectCreated:*' ], QueueArn: 'arn' } ] }
},
DependsOn: [ 'Dependent' ]
});

test.done();
}
};
13 changes: 10 additions & 3 deletions packages/@aws-cdk/aws-sns/lib/policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Construct, PolicyDocument } from '@aws-cdk/cdk';
import { Construct, IDependable, PolicyDocument } from '@aws-cdk/cdk';
import { cloudformation } from './sns.generated';
import { TopicRef } from './topic-ref';

Expand All @@ -12,18 +12,25 @@ export interface TopicPolicyProps {
/**
* Applies a policy to SNS topics.
*/
export class TopicPolicy extends Construct {
export class TopicPolicy extends Construct implements IDependable {
/**
* The IAM policy document for this policy.
*/
public readonly document = new PolicyDocument();

/**
* Allows topic policy to be added as a dependency.
*/
public readonly dependencyElements = new Array<IDependable>();

constructor(parent: Construct, name: string, props: TopicPolicyProps) {
super(parent, name);

new cloudformation.TopicPolicyResource(this, 'Resource', {
const resource = new cloudformation.TopicPolicyResource(this, 'Resource', {
policyDocument: this.document,
topics: props.topics.map(t => t.topicArn)
});

this.dependencyElements.push(resource);
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-sns/lib/topic-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ export abstract class TopicRef extends cdk.Construct implements events.IEventRul

return {
arn: this.topicArn,
type: s3n.BucketNotificationDestinationType.Topic
type: s3n.BucketNotificationDestinationType.Topic,
dependencies: [ this.policy! ] // make sure the topic policy resource is created before the notification config
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@
}
]
}
}
},
"DependsOn": [
"ObjectCreatedTopicPolicyA938ECFC",
"ObjectDeletedTopicPolicy026B02E6"
]
},
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": {
"Type": "AWS::IAM::Role",
Expand Down
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-sns/test/test.sns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,9 @@ export = {
test.deepEqual(resolve(dest1.arn), resolve(topic.topicArn));
test.deepEqual(dest1.type, s3n.BucketNotificationDestinationType.Topic);

const dep: cdk.Construct = dest1.dependencies![0] as any;
test.deepEqual((dep.children[0] as any).logicalId, 'MyTopicPolicy12A5EC17', 'verify topic policy is added as dependency');

// calling again on the same bucket yields is idempotent
const dest2 = topic.asBucketNotificationDestination(bucketArn, bucketId);
test.deepEqual(resolve(dest2.arn), resolve(topic.topicArn));
Expand Down
13 changes: 10 additions & 3 deletions packages/@aws-cdk/aws-sqs/lib/policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Construct, PolicyDocument } from '@aws-cdk/cdk';
import { Construct, IDependable, PolicyDocument } from '@aws-cdk/cdk';
import { QueueRef } from './queue-ref';
import { cloudformation } from './sqs.generated';

Expand All @@ -12,18 +12,25 @@ export interface QueuePolicyProps {
/**
* Applies a policy to SQS queues.
*/
export class QueuePolicy extends Construct {
export class QueuePolicy extends Construct implements IDependable {
/**
* The IAM policy document for this policy.
*/
public readonly document = new PolicyDocument();

/**
* Allows adding QueuePolicy as a dependency.
*/
public readonly dependencyElements = new Array<IDependable>();

constructor(parent: Construct, name: string, props: QueuePolicyProps) {
super(parent, name);

new cloudformation.QueuePolicyResource(this, 'Resource', {
const resource = new cloudformation.QueuePolicyResource(this, 'Resource', {
policyDocument: this.document,
queues: props.queues.map(q => q.queueUrl)
});

this.dependencyElements.push(resource);
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-sqs/lib/queue-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export abstract class QueueRef extends cdk.Construct implements s3n.IBucketNotif

return {
arn: this.queueArn,
type: s3n.BucketNotificationDestinationType.Queue
type: s3n.BucketNotificationDestinationType.Queue,
dependencies: [ this.policy! ]
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@
}
]
}
}
},
"DependsOn": [
"MyQueuePolicy6BBEDDAC",
"EncryptedQueuePolicy8AEB1708"
]
},
"MyQueueE6CA6235": {
"Type": "AWS::SQS::Queue"
Expand Down Expand Up @@ -227,7 +231,10 @@
}
]
}
}
},
"DependsOn": [
"MyQueuePolicy6BBEDDAC"
]
},
"EncryptedQueueKey6F4FD304": {
"Type": "AWS::KMS::Key",
Expand Down
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-sqs/test/test.sqs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ export = {
}
}));

// make sure the queue policy is added as a dependency to the bucket
// notifications resource so it will be created first.
test.deepEqual(stack.toCloudFormation().Resources.BucketNotifications8F2E257D.DependsOn, [ 'QueuePolicy25439813' ]);

test.done();
},

Expand Down