From 554a3ee0d35c64872fa33e6b44818406c588a83b Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 13 Aug 2018 21:10:08 +0300 Subject: [PATCH] feat(aws-sns): bucket notification destination Allow SNS topics to be used as bucket notification destinations. To avoid taking a dependency on aws-s3, extracted the bucket notification destination API into a separate module @aws-cdk/aws-s3-notifications, which only includes the required interfaces. We still take a devDependency on s3, but that's fine. Added examples/** to the global .nycrc --- .../@aws-cdk/aws-s3-notifications/.gitignore | 15 ++ .../@aws-cdk/aws-s3-notifications/.npmignore | 14 ++ .../@aws-cdk/aws-s3-notifications/LICENSE | 201 +++++++++++++++++ packages/@aws-cdk/aws-s3-notifications/NOTICE | 2 + .../@aws-cdk/aws-s3-notifications/README.md | 9 + .../lib/destination.ts} | 11 +- .../aws-s3-notifications/lib/index.ts | 1 + .../aws-s3-notifications/package.json | 53 +++++ packages/@aws-cdk/aws-s3/lib/bucket.ts | 2 +- packages/@aws-cdk/aws-s3/lib/index.ts | 1 - .../notifications-resource.ts | 4 +- packages/@aws-cdk/aws-s3/package.json | 3 +- .../aws-s3/test/notification-dests.ts | 14 +- .../aws-s3/test/test.notifications.ts | 19 +- packages/@aws-cdk/aws-sns/lib/topic-ref.ts | 32 ++- packages/@aws-cdk/aws-sns/package.json | 2 + ...teg.sns-bucket-notifications.expected.json | 213 ++++++++++++++++++ .../test/integ.sns-bucket-notifications.ts | 23 ++ packages/@aws-cdk/aws-sns/test/test.sns.ts | 82 +++++++ tools/cdk-build-tools/config/nycrc | 1 + 20 files changed, 676 insertions(+), 26 deletions(-) create mode 100644 packages/@aws-cdk/aws-s3-notifications/.gitignore create mode 100644 packages/@aws-cdk/aws-s3-notifications/.npmignore create mode 100644 packages/@aws-cdk/aws-s3-notifications/LICENSE create mode 100644 packages/@aws-cdk/aws-s3-notifications/NOTICE create mode 100644 packages/@aws-cdk/aws-s3-notifications/README.md rename packages/@aws-cdk/{aws-s3/lib/notification-dest.ts => aws-s3-notifications/lib/destination.ts} (63%) create mode 100644 packages/@aws-cdk/aws-s3-notifications/lib/index.ts create mode 100644 packages/@aws-cdk/aws-s3-notifications/package.json create mode 100644 packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.expected.json create mode 100644 packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.ts diff --git a/packages/@aws-cdk/aws-s3-notifications/.gitignore b/packages/@aws-cdk/aws-s3-notifications/.gitignore new file mode 100644 index 0000000000000..acfc6e27248fe --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/.gitignore @@ -0,0 +1,15 @@ +*.js +tsconfig.json +tslint.json +*.js.map +*.d.ts +*.generated.ts +dist +lib/generated/resources.ts +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-notifications/.npmignore b/packages/@aws-cdk/aws-s3-notifications/.npmignore new file mode 100644 index 0000000000000..e511c5acc268d --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/.npmignore @@ -0,0 +1,14 @@ +# 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 diff --git a/packages/@aws-cdk/aws-s3-notifications/LICENSE b/packages/@aws-cdk/aws-s3-notifications/LICENSE new file mode 100644 index 0000000000000..1739faaebb745 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/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-s3-notifications/NOTICE b/packages/@aws-cdk/aws-s3-notifications/NOTICE new file mode 100644 index 0000000000000..95fd48569c743 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/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-s3-notifications/README.md b/packages/@aws-cdk/aws-s3-notifications/README.md new file mode 100644 index 0000000000000..7d4272d8fc2f2 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/README.md @@ -0,0 +1,9 @@ +## S3 Bucket Notifications API + +This module includes the API that constructs should implement in order to be +able to be used as destinations for bucket notifications. + +To implement the `IBucketNotificationDestination`, a construct should implement +a method `asBucketNotificationDestination(bucketArn, bucketId)` which registers +this resource as a destination for bucket notifications _for the specified +bucket_ and returns the ARN of the destination and it's type. diff --git a/packages/@aws-cdk/aws-s3/lib/notification-dest.ts b/packages/@aws-cdk/aws-s3-notifications/lib/destination.ts similarity index 63% rename from packages/@aws-cdk/aws-s3/lib/notification-dest.ts rename to packages/@aws-cdk/aws-s3-notifications/lib/destination.ts index 2e16e1aa1e7f3..d48960a02c82d 100644 --- a/packages/@aws-cdk/aws-s3/lib/notification-dest.ts +++ b/packages/@aws-cdk/aws-s3-notifications/lib/destination.ts @@ -1,15 +1,18 @@ import cdk = require('@aws-cdk/cdk'); -import { Bucket } from './bucket'; /** * Implemented by constructs that can be used as bucket notification destinations. */ export interface IBucketNotificationDestination { /** - * Registers this resource to receive notifications for the specified bucket. - * @param bucket The bucket. Use the `path` of the bucket as a unique ID. + * Registers this resource to receive notifications for the specified + * bucket. This method will only be called once for each destination/bucket + * pair and the result will be cached, so there is no need to implement + * idempotency in each destination. + * @param bucketArn The ARN of the bucket + * @param bucketId A unique ID of this bucket in the stack */ - asBucketNotificationDestination(bucket: Bucket): BucketNotificationDestinationProps; + asBucketNotificationDestination(bucketArn: cdk.Arn, bucketId: string): BucketNotificationDestinationProps; } /** diff --git a/packages/@aws-cdk/aws-s3-notifications/lib/index.ts b/packages/@aws-cdk/aws-s3-notifications/lib/index.ts new file mode 100644 index 0000000000000..35b7e2954ac1f --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/lib/index.ts @@ -0,0 +1 @@ +export * from './destination'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-notifications/package.json b/packages/@aws-cdk/aws-s3-notifications/package.json new file mode 100644 index 0000000000000..390d43d9af0ba --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/package.json @@ -0,0 +1,53 @@ +{ + "name": "@aws-cdk/aws-s3-notifications", + "version": "0.8.1", + "description": "Bucket Notifications API for AWS S3", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.s3.notifications", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "s3-notifications" + } + }, + "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", + "s3", + "notifications" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "cdk-build-tools": "^0.8.1", + "pkglint": "^0.8.1" + }, + "dependencies": { + "@aws-cdk/cdk": "^0.8.1" + }, + "homepage": "https://github.com/awslabs/aws-cdk" +} diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index b2f9fb390c47b..9ecb1da168a64 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1,8 +1,8 @@ import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); +import { IBucketNotificationDestination } from '@aws-cdk/aws-s3-notifications'; import cdk = require('@aws-cdk/cdk'); import { BucketPolicy } from './bucket-policy'; -import { IBucketNotificationDestination } from './notification-dest'; import { BucketNotifications } from './notifications-resource'; import perms = require('./perms'); import { LifecycleRule } from './rule'; diff --git a/packages/@aws-cdk/aws-s3/lib/index.ts b/packages/@aws-cdk/aws-s3/lib/index.ts index e0d99e5e7ebe1..593c797757b3f 100644 --- a/packages/@aws-cdk/aws-s3/lib/index.ts +++ b/packages/@aws-cdk/aws-s3/lib/index.ts @@ -1,7 +1,6 @@ export * from './bucket'; export * from './bucket-policy'; export * from './rule'; -export * from './notification-dest'; // AWS::S3 CloudFormation Resources: export * from './s3.generated'; 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 fb14744f32346..309f845c8df5b 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 @@ -1,6 +1,6 @@ +import { BucketNotificationDestinationType, IBucketNotificationDestination } from '@aws-cdk/aws-s3-notifications'; import cdk = require('@aws-cdk/cdk'); import { Bucket, EventType, NotificationKeyFilter } from '../bucket'; -import { BucketNotificationDestinationType, IBucketNotificationDestination } from '../notification-dest'; import { NotificationsResourceHandler } from './notifications-resource-handler'; interface NotificationsProps { @@ -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); + const targetProps = target.asBucketNotificationDestination(this.bucket.bucketArn, this.bucket.path); const commonConfig: CommonConfiguration = { Events: [ event ], Filter: renderFilters(filters), diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index f5639f2bac27e..4490542d0cb4a 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -55,7 +55,8 @@ "dependencies": { "@aws-cdk/aws-iam": "^0.8.1", "@aws-cdk/aws-kms": "^0.8.1", - "@aws-cdk/cdk": "^0.8.1" + "@aws-cdk/cdk": "^0.8.1", + "@aws-cdk/aws-s3-notifications": "^0.8.1" }, "homepage": "https://github.com/awslabs/aws-cdk" } diff --git a/packages/@aws-cdk/aws-s3/test/notification-dests.ts b/packages/@aws-cdk/aws-s3/test/notification-dests.ts index afdf24035bc4c..c707a38dfd12c 100644 --- a/packages/@aws-cdk/aws-s3/test/notification-dests.ts +++ b/packages/@aws-cdk/aws-s3/test/notification-dests.ts @@ -1,11 +1,11 @@ +import s3notifications = require('@aws-cdk/aws-s3-notifications'); import cdk = require('@aws-cdk/cdk'); -import s3 = require('../lib'); /** * Since we can't take a dependency on @aws-cdk/sns, this is a simple wrapper * for AWS::SNS::Topic which implements IBucketNotificationDestination. */ -export class Topic extends cdk.Construct implements s3.IBucketNotificationDestination { +export class Topic extends cdk.Construct implements s3notifications.IBucketNotificationDestination { public readonly topicArn: cdk.Arn; private readonly policy = new cdk.PolicyDocument(); private readonly notifyingBucketPaths = new Set(); @@ -26,22 +26,22 @@ export class Topic extends cdk.Construct implements s3.IBucketNotificationDestin this.topicArn = resource.ref; } - public asBucketNotificationDestination(bucket: s3.Bucket): s3.BucketNotificationDestinationProps { + public asBucketNotificationDestination(bucketArn: cdk.Arn, bucketId: string): s3notifications.BucketNotificationDestinationProps { // add permission to each source bucket - if (!this.notifyingBucketPaths.has(bucket.path)) { + if (!this.notifyingBucketPaths.has(bucketId)) { this.policy.addStatement(new cdk.PolicyStatement() .describe(`sid${this.policy.statementCount}`) .addServicePrincipal('s3.amazonaws.com') .addAction('sns:Publish') .addResource(this.topicArn) - .addCondition('ArnLike', { "aws:SourceArn": bucket.bucketArn })); - this.notifyingBucketPaths.add(bucket.path); + .addCondition('ArnLike', { "aws:SourceArn": bucketArn })); + this.notifyingBucketPaths.add(bucketId); } return { arn: this.topicArn, - type: s3.BucketNotificationDestinationType.Topic + type: s3notifications.BucketNotificationDestinationType.Topic }; } } diff --git a/packages/@aws-cdk/aws-s3/test/test.notifications.ts b/packages/@aws-cdk/aws-s3/test/test.notifications.ts index adbd7e7fef925..d8e210fe4fa30 100644 --- a/packages/@aws-cdk/aws-s3/test/test.notifications.ts +++ b/packages/@aws-cdk/aws-s3/test/test.notifications.ts @@ -1,4 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import s3n = require('@aws-cdk/aws-s3-notifications'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import s3 = require('../lib'); @@ -93,23 +94,23 @@ export = { const bucket = new s3.Bucket(stack, 'TestBucket'); - const queueTarget: s3.IBucketNotificationDestination = { + const queueTarget: s3n.IBucketNotificationDestination = { asBucketNotificationDestination: _ => ({ - type: s3.BucketNotificationDestinationType.Queue, + type: s3n.BucketNotificationDestinationType.Queue, arn: new cdk.Arn('arn:aws:sqs:...') }) }; - const lambdaTarget: s3.IBucketNotificationDestination = { + const lambdaTarget: s3n.IBucketNotificationDestination = { asBucketNotificationDestination: _ => ({ - type: s3.BucketNotificationDestinationType.Lambda, + type: s3n.BucketNotificationDestinationType.Lambda, arn: new cdk.Arn('arn:aws:lambda:...') }) }; - const topicTarget: s3.IBucketNotificationDestination = { + const topicTarget: s3n.IBucketNotificationDestination = { asBucketNotificationDestination: _ => ({ - type: s3.BucketNotificationDestinationType.Topic, + type: s3n.BucketNotificationDestinationType.Topic, arn: new cdk.Arn('arn:aws:sns:...') }) }; @@ -176,14 +177,14 @@ export = { bucket.onEvent(s3.EventType.ObjectRemovedDelete, { asBucketNotificationDestination: _ => ({ - type: s3.BucketNotificationDestinationType.Queue, + type: s3n.BucketNotificationDestinationType.Queue, arn: new cdk.Arn('arn:aws:sqs:...:queue1') }) }); bucket.onEvent(s3.EventType.ObjectRemovedDelete, { asBucketNotificationDestination: _ => ({ - type: s3.BucketNotificationDestinationType.Queue, + type: s3n.BucketNotificationDestinationType.Queue, arn: new cdk.Arn('arn:aws:sqs:...:queue2') }) }); @@ -225,7 +226,7 @@ export = { const bucket = new s3.Bucket(stack, 'TestBucket'); const bucketNotificationTarget = { - type: s3.BucketNotificationDestinationType.Queue, + type: s3n.BucketNotificationDestinationType.Queue, arn: new cdk.Arn('arn:aws:sqs:...') }; diff --git a/packages/@aws-cdk/aws-sns/lib/topic-ref.ts b/packages/@aws-cdk/aws-sns/lib/topic-ref.ts index c0a36dcb12dda..c5476d05c4cb9 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-ref.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-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 lambda = require('@aws-cdk/aws-lambda'); +import s3n = require('@aws-cdk/aws-s3-notifications'); import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); import { TopicPolicy } from './policy'; @@ -16,7 +17,7 @@ export class TopicArn extends cdk.Arn { } /** * Either a new or imported Topic */ -export abstract class TopicRef extends cdk.Construct implements events.IEventRuleTarget, cloudwatch.IAlarmAction { +export abstract class TopicRef extends cdk.Construct implements events.IEventRuleTarget, cloudwatch.IAlarmAction, s3n.IBucketNotificationDestination { /** * Import a Topic defined elsewhere */ @@ -37,6 +38,9 @@ export abstract class TopicRef extends cdk.Construct implements events.IEventRul private policy?: TopicPolicy; + /** Buckets permitted to send notifications to this topic */ + private readonly notifyingBuckets = new Set(); + /** * Indicates if the resource policy that allows CloudWatch events to publish * notifications to this topic have been added. @@ -275,6 +279,32 @@ export abstract class TopicRef extends cdk.Construct implements events.IEventRul public metricNumberOfMessagesDelivered(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return this.metric('NumberOfMessagesDelivered', { statistic: 'sum', ...props }); } + + /** + * Implements the IBucketNotificationDestination interface, allowing topics to be used + * as bucket notification destinations. + * + * @param bucketArn The ARN of the bucket sending the notifications + * @param bucketId A unique ID of the bucket + */ + public asBucketNotificationDestination(bucketArn: cdk.Arn, bucketId: string): s3n.BucketNotificationDestinationProps { + // allow this bucket to sns:publish to this topic (if it doesn't already have a permission) + if (!this.notifyingBuckets.has(bucketId)) { + + this.addToResourcePolicy(new cdk.PolicyStatement() + .addServicePrincipal('s3.amazonaws.com') + .addAction('sns:Publish') + .addResource(this.topicArn) + .addCondition('ArnLike', { "aws:SourceArn": bucketArn })); + + this.notifyingBuckets.add(bucketId); + } + + return { + arn: this.topicArn, + type: s3n.BucketNotificationDestinationType.Topic + }; + } } /** diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index ecd7c5fda6441..0894e119e8399 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -47,6 +47,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.8.1", + "@aws-cdk/aws-s3": "^0.8.1", "cdk-build-tools": "^0.8.1", "cdk-integ-tools": "^0.8.1", "cfn2ts": "^0.8.1", @@ -58,6 +59,7 @@ "@aws-cdk/aws-iam": "^0.8.1", "@aws-cdk/aws-lambda": "^0.8.1", "@aws-cdk/aws-sqs": "^0.8.1", + "@aws-cdk/aws-s3-notifications": "^0.8.1", "@aws-cdk/cdk": "^0.8.1" }, "homepage": "https://github.com/awslabs/aws-cdk" diff --git a/packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.expected.json b/packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.expected.json new file mode 100644 index 0000000000000..011685cf9af87 --- /dev/null +++ b/packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.expected.json @@ -0,0 +1,213 @@ +{ + "Resources": { + "ObjectCreatedTopic92F47E19": { + "Type": "AWS::SNS::Topic" + }, + "ObjectCreatedTopicPolicyA938ECFC": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": { + "Ref": "ObjectCreatedTopic92F47E19" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "ObjectCreatedTopic92F47E19" + } + ] + } + }, + "ObjectDeletedTopic2A914EC0": { + "Type": "AWS::SNS::Topic" + }, + "ObjectDeletedTopicPolicy026B02E6": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": { + "Ref": "ObjectDeletedTopic2A914EC0" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "ObjectDeletedTopic2A914EC0" + } + ] + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket" + }, + "MyBucketNotifications46AC0CD2": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "MyBucketF68F3FF0" + }, + "NotificationConfiguration": { + "TopicConfigurations": [ + { + "Events": [ + "s3:ObjectCreated:*" + ], + "TopicArn": { + "Ref": "ObjectCreatedTopic92F47E19" + } + }, + { + "Events": [ + "s3:ObjectRemoved:*" + ], + "Filter": { + "Key": { + "FilterRules": [ + { + "Name": "suffix", + "Value": ".txt" + }, + { + "Name": "prefix", + "Value": "foo/" + } + ] + } + }, + "TopicArn": { + "Ref": "ObjectDeletedTopic2A914EC0" + } + } + ] + } + } + }, + "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-sns/test/integ.sns-bucket-notifications.ts b/packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.ts new file mode 100644 index 0000000000000..2376cdc41a92c --- /dev/null +++ b/packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.ts @@ -0,0 +1,23 @@ +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import sns = require('../lib'); + +class MyStack extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + const objectCreateTopic = new sns.Topic(this, 'ObjectCreatedTopic'); + const objectRemovedTopic = new sns.Topic(this, 'ObjectDeletedTopic'); + const bucket = new s3.Bucket(this, 'MyBucket'); + + bucket.onObjectCreated(objectCreateTopic); + bucket.onObjectRemoved(objectRemovedTopic, { prefix: 'foo/', suffix: '.txt' }); + + } +} + +const app = new cdk.App(process.argv); + +new MyStack(app, 'sns-bucket-notifications'); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns/test/test.sns.ts b/packages/@aws-cdk/aws-sns/test/test.sns.ts index fdaf02b8ea88b..1b44251efb67c 100644 --- a/packages/@aws-cdk/aws-sns/test/test.sns.ts +++ b/packages/@aws-cdk/aws-sns/test/test.sns.ts @@ -2,8 +2,10 @@ import { expect, haveResource } from '@aws-cdk/assert'; import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import lambda = require('@aws-cdk/aws-lambda'); +import s3n = require('@aws-cdk/aws-s3-notifications'); import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); +import { resolve } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sns = require('../lib'); @@ -687,5 +689,85 @@ export = { })); test.done(); + }, + + 'asBucketNotificationDestination adds bucket permissions only once for each bucket'(test: Test) { + const stack = new cdk.Stack(); + + const topic = new sns.Topic(stack, 'MyTopic'); + + const bucketArn = new cdk.Arn('arn:bucket'); + const bucketId = 'bucketId'; + + const dest1 = topic.asBucketNotificationDestination(bucketArn, bucketId); + test.deepEqual(resolve(dest1.arn), resolve(topic.topicArn)); + test.deepEqual(dest1.type, s3n.BucketNotificationDestinationType.Topic); + + // calling again on the same bucket yields is idempotent + const dest2 = topic.asBucketNotificationDestination(bucketArn, bucketId); + test.deepEqual(resolve(dest2.arn), resolve(topic.topicArn)); + test.deepEqual(dest2.type, s3n.BucketNotificationDestinationType.Topic); + + // another bucket will be added to the topic policy + const dest3 = topic.asBucketNotificationDestination(new cdk.Arn('bucket2'), 'bucket2'); + test.deepEqual(resolve(dest3.arn), resolve(topic.topicArn)); + test.deepEqual(dest3.type, s3n.BucketNotificationDestinationType.Topic); + + expect(stack).toMatch({ + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + }, + "MyTopicPolicy12A5EC17": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": "arn:bucket" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": { + "Ref": "MyTopic86869434" + }, + "Sid": "0" + }, + { + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": "bucket2" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": { + "Ref": "MyTopic86869434" + }, + "Sid": "1" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "MyTopic86869434" + } + ] + } + } + } + }); + + test.done(); } }; diff --git a/tools/cdk-build-tools/config/nycrc b/tools/cdk-build-tools/config/nycrc index edfbfb1b4f254..22a398e2c2183 100644 --- a/tools/cdk-build-tools/config/nycrc +++ b/tools/cdk-build-tools/config/nycrc @@ -11,6 +11,7 @@ "exclude": [ "coverage/**", "test/**", + "examples/**", "lib/*.generated.js", "build-tools/**" ]