From 876b26df73e27b37eb7468d6a31abdcfd5f42e6a Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 24 Jan 2019 16:00:15 -0800 Subject: [PATCH] feat(aws-s3): add the option to not poll to the CodePipeline Action. (#1260) --- packages/@aws-cdk/aws-cloudtrail/README.md | 24 +- .../@aws-cdk/aws-codepipeline/package.json | 3 +- .../test/integ.lambda-pipeline.expected.json | 240 +++++++++++++++++- .../test/integ.lambda-pipeline.ts | 7 +- ...eg.pipeline-cfn-cross-region.expected.json | 5 +- .../test/integ.pipeline-cfn.expected.json | 3 +- ...uild-multiple-inputs-outputs.expected.json | 3 +- .../integ.pipeline-code-deploy.expected.json | 5 +- .../test/integ.pipeline-jenkins.expected.json | 3 +- ...teg.pipeline-manual-approval.expected.json | 3 +- packages/@aws-cdk/aws-s3/README.md | 20 ++ packages/@aws-cdk/aws-s3/lib/bucket.ts | 39 +++ .../@aws-cdk/aws-s3/lib/pipeline-action.ts | 14 +- packages/@aws-cdk/aws-s3/package.json | 4 +- .../aws-s3/test/test.notifications.ts | 92 ++++++- 15 files changed, 431 insertions(+), 34 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index 024951eadb889..a67febe470617 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -31,25 +31,29 @@ For example, to log to CloudWatch Logs import cloudtrail = require('@aws-cdk/aws-cloudtrail'); const trail = new cloudtrail.CloudTrail(stack, 'CloudTrail', { - sendToCloudWatchLogs: true + sendToCloudWatchLogs: true }); ``` -This creates the same setup as above - but also logs events to a created CloudWatch Log stream. By default, the created log group has a retention period of 365 Days, but this is also configurable. +This creates the same setup as above - but also logs events to a created CloudWatch Log stream. +By default, the created log group has a retention period of 365 Days, but this is also configurable. - -For using CloudTrail event selector to log specific S3 events, you can use the `CloudTrailProps` configuration object - -For example - this logs all ReadWriteEvents for the `magic-bucket` bucket: +For using CloudTrail event selector to log specific S3 events, +you can use the `CloudTrailProps` configuration object. +Example: ```ts import cloudtrail = require('@aws-cdk/aws-cloudtrail'); -const trail = new cloudtrail.CloudTrail(stack, 'MyAmazingCloudTrail') +const trail = new cloudtrail.CloudTrail(stack, 'MyAmazingCloudTrail'); -trail.addS3Filter("arn:aws:s3:::magic-bucket/"); // Adds an event selector to the bucket magic-bucket. By default, this includes management events and all operations (Read + Write) +// Adds an event selector to the bucket magic-bucket. +// By default, this includes management events and all operations (Read + Write) +trail.addS3EventSelector(["arn:aws:s3:::magic-bucket/"]); -const configuration = { includeManagementEvents = false, readWriteType = ReadWriteType.All }; -trail.addS3Filter(["arn:aws:s3:::foo"], configuration ); // Adds an event selector to the bucket foo, with a specific configuration +// Adds an event selector to the bucket foo, with a specific configuration +trail.addS3EventSelector(["arn:aws:s3:::foo"], { + includeManagementEvents: false, + readWriteType: ReadWriteType.All, }); ``` diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index d13919c2900b5..a797d2cb9f7c1 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -62,6 +62,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.22.0", "@aws-cdk/aws-cloudformation": "^0.22.0", + "@aws-cdk/aws-cloudtrail": "^0.22.0", "@aws-cdk/aws-codebuild": "^0.22.0", "@aws-cdk/aws-codecommit": "^0.22.0", "@aws-cdk/aws-codedeploy": "^0.22.0", @@ -98,4 +99,4 @@ "construct-ctor:@aws-cdk/aws-codepipeline.CrossRegionScaffoldStack..params[1]" ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json index c8b43657dfa5c..4ec56bdc8442f 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json @@ -145,7 +145,7 @@ "Ref": "PipelineBucketB967BD35" }, "S3ObjectKey": "key", - "PollForSourceChanges": true + "PollForSourceChanges": false }, "InputArtifacts": [], "Name": "Source", @@ -188,6 +188,66 @@ "PipelineRoleDefaultPolicyC7A05455" ] }, + "PipelineEventsRole46BEEA7C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "codepipeline:StartPipelineExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "Roles": [ + { + "Ref": "PipelineEventsRole46BEEA7C" + } + ] + } + }, "PipelineBucketB967BD35": { "Type": "AWS::S3::Bucket", "Properties": { @@ -196,6 +256,182 @@ } } }, + "PipelineBucketawscdkcodepipelinelambdaPipeline87A4B3D3SourceEventRuleCE4D4505": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.s3" + ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com" + ], + "eventName": [ + "PutObject" + ], + "resources": { + "ARN": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/key" + ] + ] + } + ] + } + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + }, + "Id": "Pipeline", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineEventsRole46BEEA7C", + "Arn" + ] + } + } + ] + } + }, + "CloudTrailS310CD22F2": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, + "CloudTrailS3PolicyEA49A03E": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudTrailS310CD22F2" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetBucketAcl", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + }, + "/AWSLogs/", + { + "Ref": "AWS::AccountId" + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CloudTrailA62D711D": { + "Type": "AWS::CloudTrail::Trail", + "Properties": { + "IsLogging": true, + "S3BucketName": { + "Ref": "CloudTrailS310CD22F2" + }, + "EnableLogFileValidation": true, + "EventSelectors": [ + { + "DataResources": [ + { + "Type": "AWS::S3::Object", + "Values": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/key" + ] + ] + } + ] + } + ], + "IncludeManagementEvents": false, + "ReadWriteType": "WriteOnly" + } + ], + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true + }, + "DependsOn": [ + "CloudTrailS3PolicyEA49A03E" + ] + }, "LambdaFunServiceRoleF0979767": { "Type": "AWS::IAM::Role", "Properties": { @@ -272,4 +508,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts index 2ec36f02994b5..f75228dce9fe4 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts @@ -1,3 +1,4 @@ +import cloudtrail = require('@aws-cdk/aws-cloudtrail'); import lambda = require('@aws-cdk/aws-lambda'); import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); @@ -14,11 +15,15 @@ const bucket = new s3.Bucket(stack, 'PipelineBucket', { versioned: true, removalPolicy: cdk.RemovalPolicy.Destroy, }); +const key = 'key'; +const trail = new cloudtrail.CloudTrail(stack, 'CloudTrail'); +trail.addS3EventSelector([bucket.arnForObjects(key)], cloudtrail.ReadWriteType.WriteOnly); new s3.PipelineSourceAction(stack, 'Source', { stage: sourceStage, outputArtifactName: 'SourceArtifact', bucket, - bucketKey: 'key', + bucketKey: key, + pollForSourceChanges: false, }); const lambdaFun = new lambda.Function(stack, 'LambdaFun', { diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json index 99f7e7886620d..11d129412453c 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json @@ -161,8 +161,7 @@ "S3Bucket": { "Ref": "MyBucketF68F3FF0" }, - "S3ObjectKey": "some/path", - "PollForSourceChanges": true + "S3ObjectKey": "some/path" }, "InputArtifacts": [], "Name": "S3", @@ -245,4 +244,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json index 63ea285a6f8d4..ce6601744b4fc 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json @@ -173,8 +173,7 @@ "S3Bucket": { "Ref": "PipelineBucketB967BD35" }, - "S3ObjectKey": "key", - "PollForSourceChanges": true + "S3ObjectKey": "key" }, "InputArtifacts": [], "Name": "Source", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json index fced0f5d6d44e..edf559a6831b7 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json @@ -275,8 +275,7 @@ "S3Bucket": { "Ref": "MyBucketF68F3FF0" }, - "S3ObjectKey": "some/path", - "PollForSourceChanges": true + "S3ObjectKey": "some/path" }, "InputArtifacts": [], "Name": "Source2", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json index af9cc411c88b1..a1615f8e2905c 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json @@ -284,8 +284,7 @@ "S3Bucket": { "Ref": "CodeDeployPipelineIntegTest9F618D61" }, - "S3ObjectKey": "application.zip", - "PollForSourceChanges": true + "S3ObjectKey": "application.zip" }, "InputArtifacts": [], "Name": "S3Source", @@ -336,4 +335,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-jenkins.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-jenkins.expected.json index 56d66c4978305..fcd2e2444f8f1 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-jenkins.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-jenkins.expected.json @@ -134,8 +134,7 @@ "S3Bucket": { "Ref": "MyBucketF68F3FF0" }, - "S3ObjectKey": "some/path", - "PollForSourceChanges": true + "S3ObjectKey": "some/path" }, "InputArtifacts": [], "Name": "S3", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json index 6015e4df4dd50..d27e5e8587ea3 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json @@ -130,8 +130,7 @@ "S3Bucket": { "Ref": "Bucket83908E77" }, - "S3ObjectKey": "file.zip", - "PollForSourceChanges": true + "S3ObjectKey": "file.zip" }, "InputArtifacts": [], "Name": "S3", diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index c70f58d600df9..343795f6b2815 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -112,6 +112,26 @@ const sourceAction = sourceBucket.addToPipeline(sourceStage, 'S3Source', { }); ``` +By default, the Pipeline will poll the Bucket to detect changes. +You can change that behavior to use CloudWatch Events by setting the `pollForSourceChanges` +property to `false` (it's `true` by default). +If you do that, make sure the source Bucket is part of an AWS CloudTrail Trail - +otherwise, the CloudWatch Events will not be emitted, +and your Pipeline will not react to changes in the Bucket. +You can do it through the CDK: + +```typescript +import cloudtrail = require('@aws-cdk/aws-cloudtrail'); + +const key = 'some/key.zip'; +const trail = new cloudtrail.CloudTrail(this, 'CloudTrail'); +trail.addS3EventSelector([sourceBucket.arnForObjects(key)], cloudtrail.ReadWriteType.WriteOnly); +const sourceAction = sourceBucket.addToPipeline(sourceStage, 'S3Source', { + bucketKey: key, + pollForSourceChanges: false, // default: true +}); +``` + ### Sharing buckets between stacks To use a bucket in a different stack in the same CDK application, pass the object to the other stack: diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 1154ba5139a32..b33445332bbab 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1,4 +1,5 @@ import actions = require('@aws-cdk/aws-codepipeline-api'); +import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import { IBucketNotificationDestination } from '@aws-cdk/aws-s3-notifications'; @@ -171,6 +172,16 @@ export interface IBucket extends cdk.IConstruct { * @returns The `iam.PolicyStatement` object, which can be used to apply e.g. conditions. */ grantPublicAccess(keyPrefix?: string, ...allowedActions: string[]): iam.PolicyStatement; + + /** + * Defines a CloudWatch Event Rule that triggers upon putting an object into the Bucket. + * + * @param name the logical ID of the newly created Event Rule + * @param target the optional target of the Event Rule + * @param path the optional path inside the Bucket that will be watched for changes + * @returns a new {@link events.EventRule} instance + */ + onPutObject(name: string, target?: events.IEventRuleTarget, path?: string): events.EventRule; } /** @@ -290,6 +301,34 @@ export abstract class BucketBase extends cdk.Construct implements IBucket { }); } + public onPutObject(name: string, target?: events.IEventRuleTarget, path?: string): events.EventRule { + const eventRule = new events.EventRule(this, name, { + eventPattern: { + source: [ + 'aws.s3', + ], + detailType: [ + 'AWS API Call via CloudTrail', + ], + detail: { + eventSource: [ + 's3.amazonaws.com', + ], + eventName: [ + 'PutObject', + ], + resources: { + ARN: [ + path ? this.arnForObjects(path) : this.bucketArn, + ], + }, + }, + }, + }); + eventRule.addTarget(target); + return eventRule; + } + /** * Adds a statement to the resource policy for a principal (i.e. * account/role/service) to perform actions on this bucket and/or it's diff --git a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts b/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts index 5fb5ca522547c..44ab1f1ed1c1e 100644 --- a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts @@ -23,11 +23,14 @@ export interface CommonPipelineSourceActionProps extends codepipeline.CommonActi */ bucketKey: string; - // TODO: use CloudWatch events instead /** - * Whether or not AWS CodePipeline should poll for source changes + * Whether AWS CodePipeline should poll for source changes. + * If this is `false`, the Pipeline will use CloudWatch Events to detect source changes instead. + * Note that if this is `false`, you need to make sure to include the source Bucket in a CloudTrail Trail, + * as otherwise the CloudWatch Events will not be emitted. * * @default true + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/log-s3-data-events.html */ pollForSourceChanges?: boolean; } @@ -53,11 +56,16 @@ export class PipelineSourceAction extends codepipeline.SourceAction { configuration: { S3Bucket: props.bucket.bucketName, S3ObjectKey: props.bucketKey, - PollForSourceChanges: props.pollForSourceChanges || true + PollForSourceChanges: props.pollForSourceChanges, }, ...props, }); + if (props.pollForSourceChanges === false) { + props.bucket.onPutObject(props.stage.pipeline.node.uniqueId + 'SourceEventRule', + props.stage.pipeline, props.bucketKey); + } + // pipeline needs permissions to read from the S3 bucket props.bucket.grantRead(props.stage.pipeline.role); } diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 743b9d1686656..d5dade1103486 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -62,6 +62,7 @@ }, "dependencies": { "@aws-cdk/aws-codepipeline-api": "^0.22.0", + "@aws-cdk/aws-events": "^0.22.0", "@aws-cdk/aws-iam": "^0.22.0", "@aws-cdk/aws-kms": "^0.22.0", "@aws-cdk/aws-s3-notifications": "^0.22.0", @@ -70,6 +71,7 @@ "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/aws-codepipeline-api": "^0.22.0", + "@aws-cdk/aws-events": "^0.22.0", "@aws-cdk/aws-iam": "^0.22.0", "@aws-cdk/aws-kms": "^0.22.0", "@aws-cdk/aws-s3-notifications": "^0.22.0", @@ -87,4 +89,4 @@ "resource-interface:@aws-cdk/aws-s3.IBucketPolicy" ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-s3/test/test.notifications.ts b/packages/@aws-cdk/aws-s3/test/test.notifications.ts index 72088e68526a8..50bfa11563aeb 100644 --- a/packages/@aws-cdk/aws-s3/test/test.notifications.ts +++ b/packages/@aws-cdk/aws-s3/test/test.notifications.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import s3n = require('@aws-cdk/aws-s3-notifications'); import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; @@ -299,5 +299,93 @@ export = { }); test.done(); - } + }, + + 'CloudWatch Events': { + 'onPutItem contains the Bucket ARN itself when path is undefined'(test: Test) { + const stack = new cdk.Stack(); + const bucket = s3.Bucket.import(stack, 'Bucket', { + bucketName: 'MyBucket', + }); + bucket.onPutObject('PutRule'); + + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + "EventPattern": { + "source": [ + "aws.s3", + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com", + ], + "eventName": [ + "PutObject", + ], + "resources": { + "ARN": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::MyBucket", + ], + ], + }, + ], + }, + }, + }, + "State": "ENABLED", + })); + + test.done(); + }, + + "onPutItem contains the path when it's provided"(test: Test) { + const stack = new cdk.Stack(); + const bucket = s3.Bucket.import(stack, 'Bucket', { + bucketName: 'MyBucket', + }); + bucket.onPutObject('PutRule', undefined, 'my/path.zip'); + + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + "EventPattern": { + "source": [ + "aws.s3", + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com", + ], + "eventName": [ + "PutObject", + ], + "resources": { + "ARN": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::MyBucket/my/path.zip" + ], + ], + }, + ], + }, + }, + }, + "State": "ENABLED", + })); + + test.done(); + }, + }, };