From 28942d2115fe42d4e59e8dbbaec28197c1436e1c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 23 May 2019 16:58:38 +0200 Subject: [PATCH] fix: update all 'onXxx' methods to be CloudWatch Events (#2609) Make all CloudWatch events methods consistent. All methods called 'onXxx()' are now guaranteed to be CloudWatch Events triggers, all non-CWE methods that started with 'on' are now called differently. Introduce the 'onCloudTrailEvent' convention, to clearly signal that the indicated event depend on the presence of a CloudTrail Trail to capture it (as opposed to being intrinsically supported by the service itself). Introduce awslint rules to enforce these conventions. Fixes #2278. BREAKING CHANGES * All `onXxx()` CloudWatch Event methods now have the signature: ```ts resource.onEvent('SomeId', { target: new SomeTarget(...), // options }); ``` * CloudWatch: rename `onAlarm()` => `addAlarmAction()`, `onOk` => `addOkAction`, `onInsufficientData` => `addInsufficientDataAction`. * AutoScaling: rename `onLifecycleTransition` => `addLifecycleHook()`. * LambdaDeploymentGroup: rename `onPreHook` => `addPreHook`, `onPostHook` => `addPostHook`. * UserPool: rename all `onXxx` methods => `addXxxTrigger`. * Repository: rename `onImagePushed` => `onCloudTrailImagePushed`. * Bucket: rename `onEvent` => `addEventNotification`, `onObjectCreated` => `addObjectCreatedNotification`, `onObjectRemoved` => `addObjectRemovedNotification`, `onPutObject` => `onCloudTrailPutObject`. --- .../lib/step-scaling-policy.ts | 4 +- .../aws-autoscaling/lib/auto-scaling-group.ts | 4 +- .../lib/step-scaling-policy.ts | 4 +- .../test/test.lifecyclehooks.ts | 2 +- .../@aws-cdk/aws-cloudtrail/SAMPLE-EVENTS.md | 68 ++++ packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 14 +- .../aws-cloudtrail/test/test.cloudtrail.ts | 12 +- packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 6 +- .../aws-cloudwatch/test/test.alarm.ts | 6 +- .../@aws-cdk/aws-codebuild/lib/project.ts | 68 ++-- .../aws-codebuild/test/test.codebuild.ts | 10 +- .../@aws-cdk/aws-codecommit/lib/repository.ts | 77 ++-- .../test/integ.codecommit-events.ts | 10 +- .../lib/lambda/deployment-group.ts | 8 +- .../test/lambda/test.deployment-group.ts | 8 +- .../lib/codecommit/source-action.ts | 6 +- .../lib/ecr/source-action.ts | 10 +- .../lib/s3/source-action.ts | 9 +- .../cloudformation/test.pipeline-actions.ts | 7 + .../test/integ.lambda-pipeline.expected.json | 5 +- .../integ.pipeline-ecr-source.expected.json | 5 +- .../test/integ.pipeline-events.ts | 8 +- .../test/test.pipeline.ts | 5 +- .../@aws-cdk/aws-codepipeline/lib/action.ts | 17 + .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 56 +-- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 16 +- .../aws-cognito/test/test.user-pool.ts | 18 +- packages/@aws-cdk/aws-config/README.md | 4 +- packages/@aws-cdk/aws-config/lib/rule.ts | 18 +- .../aws-config/test/integ.rule.lit.ts | 4 +- .../@aws-cdk/aws-config/test/test.rule.ts | 4 +- packages/@aws-cdk/aws-ecr/lib/repository.ts | 90 ++++- .../@aws-cdk/aws-ecr/test/test.repository.ts | 11 +- .../lib/drain-hook/instance-drain-hook.ts | 2 +- .../test/codebuild/integ.project-events.ts | 16 +- .../integ.pipeline-event-target.expected.json | 380 ++++++++++++++++++ .../integ.pipeline-event-target.ts | 50 +++ packages/@aws-cdk/aws-events/lib/index.ts | 1 + .../aws-events/lib/on-event-options.ts | 36 ++ packages/@aws-cdk/aws-events/lib/util.ts | 4 +- .../aws-lambda-event-sources/lib/s3.ts | 2 +- .../test/integ.bucket-notifications.ts | 4 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 111 +++-- .../aws-s3/test/integ.notifications.ts | 6 +- .../aws-s3/test/test.notifications.ts | 39 +- .../test/integ.sns-bucket-notifications.ts | 4 +- .../test/integ.bucket-notifications.ts | 6 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 6 +- .../aws-stepfunctions/lib/states/state.ts | 6 +- .../aws-stepfunctions/lib/states/task.ts | 4 +- tools/awslint/bin/awslint.ts | 6 +- tools/awslint/lib/linter.ts | 26 +- tools/awslint/lib/rules/cloudwatch-events.ts | 71 ++++ tools/awslint/lib/rules/index.ts | 1 + 54 files changed, 1090 insertions(+), 285 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudtrail/SAMPLE-EVENTS.md create mode 100644 packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json create mode 100644 packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts create mode 100644 packages/@aws-cdk/aws-events/lib/on-event-options.ts create mode 100644 tools/awslint/lib/rules/cloudwatch-events.ts diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts index 80cb19e90e620..7ae93532ecde8 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts @@ -108,7 +108,7 @@ export class StepScalingPolicy extends cdk.Construct { evaluationPeriods: 1, threshold, }); - this.lowerAlarm.onAlarm(this.lowerAction); + this.lowerAlarm.addAlarmAction(this.lowerAction); } if (alarms.upperAlarmIntervalIndex !== undefined) { @@ -138,7 +138,7 @@ export class StepScalingPolicy extends cdk.Construct { evaluationPeriods: 1, threshold, }); - this.upperAlarm.onAlarm(this.upperAction); + this.upperAlarm.addAlarmAction(this.upperAction); } } } diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 238899b972c08..415e12e31bc95 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -193,7 +193,7 @@ abstract class AutoScalingGroupBase extends Resource implements IAutoScalingGrou /** * Send a message to either an SQS queue or SNS topic when instances launch or terminate */ - public onLifecycleTransition(id: string, props: BasicLifecycleHookProps): LifecycleHook { + public addLifecycleHook(id: string, props: BasicLifecycleHookProps): LifecycleHook { return new LifecycleHook(this, `LifecycleHook${id}`, { autoScalingGroup: this, ...props @@ -701,7 +701,7 @@ export interface IAutoScalingGroup extends IResource { /** * Send a message to either an SQS queue or SNS topic when instances launch or terminate */ - onLifecycleTransition(id: string, props: BasicLifecycleHookProps): LifecycleHook; + addLifecycleHook(id: string, props: BasicLifecycleHookProps): LifecycleHook; /** * Scale out or in based on time diff --git a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts index e85e6f3546107..7fb1bb9c60f9c 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts @@ -109,7 +109,7 @@ export class StepScalingPolicy extends cdk.Construct { evaluationPeriods: 1, threshold, }); - this.lowerAlarm.onAlarm(this.lowerAction); + this.lowerAlarm.addAlarmAction(this.lowerAction); } if (alarms.upperAlarmIntervalIndex !== undefined) { @@ -139,7 +139,7 @@ export class StepScalingPolicy extends cdk.Construct { evaluationPeriods: 1, threshold, }); - this.upperAlarm.onAlarm(this.upperAction); + this.upperAlarm.addAlarmAction(this.upperAction); } } } diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.lifecyclehooks.ts b/packages/@aws-cdk/aws-autoscaling/test/test.lifecyclehooks.ts index e680224545f96..276cc35e22fff 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.lifecyclehooks.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.lifecyclehooks.ts @@ -18,7 +18,7 @@ export = { }); // WHEN - asg.onLifecycleTransition('Transition', { + asg.addLifecycleHook('Transition', { notificationTarget: new FakeNotificationTarget(), lifecycleTransition: autoscaling.LifecycleTransition.InstanceLaunching, defaultResult: autoscaling.DefaultResult.Abandon, diff --git a/packages/@aws-cdk/aws-cloudtrail/SAMPLE-EVENTS.md b/packages/@aws-cdk/aws-cloudtrail/SAMPLE-EVENTS.md new file mode 100644 index 0000000000000..f729b5f8a0003 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail/SAMPLE-EVENTS.md @@ -0,0 +1,68 @@ +# Some sample CloudTrail events + +For reference. + + +## S3 + +PutObject + + { + "eventSource": "s3.amazonaws.com", + "resources": [ + { + "ARN": "arn:aws:s3:::BUCKETNAME/OBJECTKEY", + "type": "AWS::S3::Object" + }, + { + "accountId": "123456789012", + "ARN": "arn:aws:s3:::BUCKETNAME", + "type": "AWS::S3::Bucket" + } + ], + "eventTime": "2019-05-22T08:38:05Z", + "userAgent": "[aws-cli/1.16.96 Python/2.7.12 Linux/4.4.0-146-generic botocore/1.12.86]", + "readOnly": false, + "recipientAccountId": "123456789012", + "awsRegion": "eu-west-1", + "requestID": "CF9748DFDC5FB0A4", + "additionalEventData": { + "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", + "bytesTransferredOut": 0, + "AuthenticationMethod": "AuthHeader", + "x-amz-id-2": "hRJMAs5p4ALZIabP4ATIL53npWU61+N6LYWj02gdQtR0ymKSySzVXUSZx7ydv7tRJwk+XMaPerM=", + "bytesTransferredIn": 197, + "SignatureVersion": "SigV4" + }, + "eventType": "AwsApiCall", + "eventID": "3074546e-1bfa-4973-8502-b1bb4d0bda1a", + "eventVersion": "1.05", + "eventName": "PutObject", + "sourceIPAddress": "1.2.3.4", + "userIdentity": { + "accountId": "123456789012", + "type": "AssumedRole", + "principalId": "AROAJBNCAL3UTR5C42U4M:user-SomeRole", + "accessKeyId": "AZYCAIJERO6H7", + "sessionContext": { + "attributes": { + "mfaAuthenticated": "false", + "creationDate": "2019-05-22T08:10:21Z" + }, + "sessionIssuer": { + "accountId": "123456789012", + "type": "Role", + "principalId": "AROAJBNCAL3UTR5C42U4M", + "userName": "SomeRole", + "arn": "arn:aws:iam::123456789012:role/SomeRole" + } + }, + "arn": "arn:aws:sts::123456789012:assumed-role/SomeRole/user-SomeRole" + }, + "responseElements": null, + "requestParameters": { + "bucketName": "BUCKETNAME", + "Host": "BUCKETNAME.s3.eu-west-1.amazonaws.com", + "key": "OBJECTKEY" + } + } diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index da8ce991cfc86..6c8001a8712fa 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -222,14 +222,16 @@ export class Trail extends Resource { } /** - * Create an event rule for when an event is recorded by any trail. + * Create an event rule for when an event is recorded by any Trail in the account. * - * Note that the event doesn't necessarily have to come from this - * trail. Be sure to filter the event properly using an event pattern. + * Note that the event doesn't necessarily have to come from this Trail, it can + * be captured from any one. + * + * Be sure to filter the event further down using an event pattern. */ - public onEvent(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = new events.Rule(this, name, options); - rule.addTarget(target); + public onCloudTrailEvent(id: string, options: events.OnEventOptions): events.Rule { + const rule = new events.Rule(this, id, options); + rule.addTarget(options.target); rule.addEventPattern({ detailType: ['AWS API Call via CloudTrail'] }); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index facae5fe9003b..605cc9f721033 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -197,11 +197,13 @@ export = { const trail = new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WriteOnly }); // WHEN - trail.onEvent('DoEvents', { - bind: () => ({ - arn: 'arn', - id: 'myid' - }) + trail.onCloudTrailEvent('DoEvents', { + target: { + bind: () => ({ + arn: 'arn', + id: 'myid' + }) + } }); // THEN diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 2f5929fc853d3..923b43492c154 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -155,7 +155,7 @@ export class Alarm extends Resource implements IAlarm { * * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. */ - public onAlarm(...actions: IAlarmAction[]) { + public addAlarmAction(...actions: IAlarmAction[]) { if (this.alarmActionArns === undefined) { this.alarmActionArns = []; } @@ -168,7 +168,7 @@ export class Alarm extends Resource implements IAlarm { * * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. */ - public onInsufficientData(...actions: IAlarmAction[]) { + public addInsufficientDataAction(...actions: IAlarmAction[]) { if (this.insufficientDataActionArns === undefined) { this.insufficientDataActionArns = []; } @@ -181,7 +181,7 @@ export class Alarm extends Resource implements IAlarm { * * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. */ - public onOk(...actions: IAlarmAction[]) { + public addOkAction(...actions: IAlarmAction[]) { if (this.okActionArns === undefined) { this.okActionArns = []; } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts index 5ef3dfe2a4307..0f5268b8b61b5 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts @@ -72,9 +72,9 @@ export = { evaluationPeriods: 2 }); - alarm.onAlarm(new TestAlarmAction('A')); - alarm.onInsufficientData(new TestAlarmAction('B')); - alarm.onOk(new TestAlarmAction('C')); + alarm.addAlarmAction(new TestAlarmAction('A')); + alarm.addInsufficientDataAction(new TestAlarmAction('B')); + alarm.addOkAction(new TestAlarmAction('C')); // THEN expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index faebadbb2de75..b7535f8af7bb2 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -32,6 +32,13 @@ export interface IProject extends IResource, iam.IGrantable { /** The IAM service Role of this Project. Undefined for imported Projects. */ readonly role?: iam.IRole; + /** + * Defines a CloudWatch event rule triggered when something happens with this project. + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html + */ + onEvent(id: string, options: events.OnEventOptions): events.Rule; + /** * Defines a CloudWatch event rule triggered when the build project state * changes. You can filter specific build status events using an event @@ -57,7 +64,7 @@ export interface IProject extends IResource, iam.IGrantable { * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html */ - onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onStateChange(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule that triggers upon phase change of this @@ -65,22 +72,22 @@ export interface IProject extends IResource, iam.IGrantable { * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html */ - onPhaseChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onPhaseChange(id: string, options: events.OnEventOptions): events.Rule; /** * Defines an event rule which triggers when a build starts. */ - onBuildStarted(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onBuildStarted(id: string, options: events.OnEventOptions): events.Rule; /** * Defines an event rule which triggers when a build fails. */ - onBuildFailed(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onBuildFailed(id: string, options: events.OnEventOptions): events.Rule; /** * Defines an event rule which triggers when a build completes successfully. */ - onBuildSucceeded(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onBuildSucceeded(id: string, options: events.OnEventOptions): events.Rule; /** * @returns a CloudWatch metric associated with this build project. @@ -157,6 +164,23 @@ abstract class ProjectBase extends Resource implements IProject { /** The IAM service Role of this Project. */ public abstract readonly role?: iam.IRole; + /** + * Defines a CloudWatch event rule triggered when something happens with this project. + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html + */ + public onEvent(id: string, options: events.OnEventOptions): events.Rule { + const rule = new events.Rule(this, id, options); + rule.addTarget(options.target); + rule.addEventPattern({ + source: ['aws.codebuild'], + detail: { + 'project-name': [this.projectName] + } + }); + return rule; + } + /** * Defines a CloudWatch event rule triggered when the build project state * changes. You can filter specific build status events using an event @@ -182,17 +206,10 @@ abstract class ProjectBase extends Resource implements IProject { * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html */ - public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = new events.Rule(this, name, options); - rule.addTarget(target); + public onStateChange(id: string, options: events.OnEventOptions) { + const rule = this.onEvent(id, options); rule.addEventPattern({ - source: ['aws.codebuild'], detailType: ['CodeBuild Build State Change'], - detail: { - 'project-name': [ - this.projectName - ] - } }); return rule; } @@ -203,17 +220,10 @@ abstract class ProjectBase extends Resource implements IProject { * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html */ - public onPhaseChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = new events.Rule(this, name, options); - rule.addTarget(target); + public onPhaseChange(id: string, options: events.OnEventOptions) { + const rule = this.onEvent(id, options); rule.addEventPattern({ - source: ['aws.codebuild'], detailType: ['CodeBuild Build Phase Change'], - detail: { - 'project-name': [ - this.projectName - ] - } }); return rule; } @@ -224,8 +234,8 @@ abstract class ProjectBase extends Resource implements IProject { * To access fields from the event in the event target input, * use the static fields on the `StateChangeEvent` class. */ - public onBuildStarted(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onStateChange(name, target, options); + public onBuildStarted(id: string, options: events.OnEventOptions) { + const rule = this.onStateChange(id, options); rule.addEventPattern({ detail: { 'build-status': ['IN_PROGRESS'] @@ -240,8 +250,8 @@ abstract class ProjectBase extends Resource implements IProject { * To access fields from the event in the event target input, * use the static fields on the `StateChangeEvent` class. */ - public onBuildFailed(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onStateChange(name, target, options); + public onBuildFailed(id: string, options: events.OnEventOptions) { + const rule = this.onStateChange(id, options); rule.addEventPattern({ detail: { 'build-status': ['FAILED'] @@ -256,8 +266,8 @@ abstract class ProjectBase extends Resource implements IProject { * To access fields from the event in the event target input, * use the static fields on the `StateChangeEvent` class. */ - public onBuildSucceeded(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onStateChange(name, target, options); + public onBuildSucceeded(id: string, options: events.OnEventOptions) { + const rule = this.onStateChange(id, options); rule.addEventPattern({ detail: { 'build-status': ['SUCCEEDED'] diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 7893113869115..bb4149c8ad9f4 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -973,11 +973,11 @@ export = { source: new codebuild.CodePipelineSource() }); - project.onBuildFailed('OnBuildFailed'); - project.onBuildSucceeded('OnBuildSucceeded'); - project.onPhaseChange('OnPhaseChange'); - project.onStateChange('OnStateChange'); - project.onBuildStarted('OnBuildStarted'); + project.onBuildFailed('OnBuildFailed', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) }}); + project.onBuildSucceeded('OnBuildSucceeded', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) }}); + project.onPhaseChange('OnPhaseChange', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) }}); + project.onStateChange('OnStateChange', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) }}); + project.onBuildStarted('OnBuildStarted', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) }}); expect(stack).to(haveResource('AWS::Events::Rule', { "EventPattern": { diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 518f6ffb0a8a6..03e2372b5e7ba 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -31,53 +31,64 @@ export interface IRepository extends IResource { * Defines a CloudWatch event rule which triggers for repository events. Use * `rule.addEventPattern(pattern)` to specify a filter. */ - onEvent(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onEvent(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a "CodeCommit * Repository State Change" event occurs. */ - onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onStateChange(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a reference is * created (i.e. a new branch/tag is created) to the repository. */ - onReferenceCreated(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onReferenceCreated(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a reference is * updated (i.e. a commit is pushed to an existing or new branch) from the repository. */ - onReferenceUpdated(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onReferenceUpdated(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a reference is * delete (i.e. a branch/tag is deleted) from the repository. */ - onReferenceDeleted(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onReferenceDeleted(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a pull request state is changed. */ - onPullRequestStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onPullRequestStateChange(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a comment is made on a pull request. */ - onCommentOnPullRequest(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onCommentOnPullRequest(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a comment is made on a commit. */ - onCommentOnCommit(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onCommentOnCommit(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers when a commit is pushed to a branch. - * @param target The target of the event - * @param branch The branch to monitor. Defaults to all branches. */ - onCommit(name: string, target?: events.IRuleTarget, branch?: string): events.Rule; + onCommit(id: string, options: OnCommitOptions): events.Rule; +} + +/** + * Options for the onCommit() method + */ +export interface OnCommitOptions extends events.OnEventOptions { + + /** + * The branch to monitor. + * + * @default - All branches + */ + readonly branches?: string[]; } /** @@ -106,13 +117,13 @@ abstract class RepositoryBase extends Resource implements IRepository { * Defines a CloudWatch event rule which triggers for repository events. Use * `rule.addEventPattern(pattern)` to specify a filter. */ - public onEvent(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = new events.Rule(this, name, options); + public onEvent(id: string, options: events.OnEventOptions) { + const rule = new events.Rule(this, id, options); rule.addEventPattern({ source: [ 'aws.codecommit' ], resources: [ this.repositoryArn ] }); - rule.addTarget(target); + rule.addTarget(options.target); return rule; } @@ -120,8 +131,8 @@ abstract class RepositoryBase extends Resource implements IRepository { * Defines a CloudWatch event rule which triggers when a "CodeCommit * Repository State Change" event occurs. */ - public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onEvent(name, target, options); + public onStateChange(id: string, options: events.OnEventOptions) { + const rule = this.onEvent(id, options); rule.addEventPattern({ detailType: [ 'CodeCommit Repository State Change' ], }); @@ -132,8 +143,8 @@ abstract class RepositoryBase extends Resource implements IRepository { * Defines a CloudWatch event rule which triggers when a reference is * created (i.e. a new branch/tag is created) to the repository. */ - public onReferenceCreated(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onStateChange(name, target, options); + public onReferenceCreated(id: string, options: events.OnEventOptions) { + const rule = this.onStateChange(id, options); rule.addEventPattern({ detail: { event: [ 'referenceCreated' ] } }); return rule; } @@ -142,8 +153,8 @@ abstract class RepositoryBase extends Resource implements IRepository { * Defines a CloudWatch event rule which triggers when a reference is * updated (i.e. a commit is pushed to an existing or new branch) from the repository. */ - public onReferenceUpdated(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onStateChange(name, target, options); + public onReferenceUpdated(id: string, options: events.OnEventOptions) { + const rule = this.onStateChange(id, options); rule.addEventPattern({ detail: { event: [ 'referenceCreated', 'referenceUpdated' ] } }); return rule; } @@ -152,8 +163,8 @@ abstract class RepositoryBase extends Resource implements IRepository { * Defines a CloudWatch event rule which triggers when a reference is * delete (i.e. a branch/tag is deleted) from the repository. */ - public onReferenceDeleted(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onStateChange(name, target, options); + public onReferenceDeleted(id: string, options: events.OnEventOptions) { + const rule = this.onStateChange(id, options); rule.addEventPattern({ detail: { event: [ 'referenceDeleted' ] } }); return rule; } @@ -161,8 +172,8 @@ abstract class RepositoryBase extends Resource implements IRepository { /** * Defines a CloudWatch event rule which triggers when a pull request state is changed. */ - public onPullRequestStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onEvent(name, target, options); + public onPullRequestStateChange(id: string, options: events.OnEventOptions) { + const rule = this.onEvent(id, options); rule.addEventPattern({ detailType: [ 'CodeCommit Pull Request State Change' ] }); return rule; } @@ -170,8 +181,8 @@ abstract class RepositoryBase extends Resource implements IRepository { /** * Defines a CloudWatch event rule which triggers when a comment is made on a pull request. */ - public onCommentOnPullRequest(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onEvent(name, target, options); + public onCommentOnPullRequest(id: string, options: events.OnEventOptions) { + const rule = this.onEvent(id, options); rule.addEventPattern({ detailType: [ 'CodeCommit Comment on Pull Request' ] }); return rule; } @@ -179,21 +190,19 @@ abstract class RepositoryBase extends Resource implements IRepository { /** * Defines a CloudWatch event rule which triggers when a comment is made on a commit. */ - public onCommentOnCommit(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { - const rule = this.onEvent(name, target, options); + public onCommentOnCommit(id: string, options: events.OnEventOptions) { + const rule = this.onEvent(id, options); rule.addEventPattern({ detailType: [ 'CodeCommit Comment on Commit' ] }); return rule; } /** * Defines a CloudWatch event rule which triggers when a commit is pushed to a branch. - * @param target The target of the event - * @param branch The branch to monitor. Defaults to all branches. */ - public onCommit(name: string, target?: events.IRuleTarget, branch?: string) { - const rule = this.onReferenceUpdated(name, target); - if (branch) { - rule.addEventPattern({ detail: { referenceName: [ branch ] }}); + public onCommit(id: string, options: OnCommitOptions) { + const rule = this.onReferenceUpdated(id, options); + if (options.branches) { + rule.addEventPattern({ detail: { referenceName: options.branches }}); } return rule; } diff --git a/packages/@aws-cdk/aws-codecommit/test/integ.codecommit-events.ts b/packages/@aws-cdk/aws-codecommit/test/integ.codecommit-events.ts index b674b18656260..654dde70c1684 100644 --- a/packages/@aws-cdk/aws-codecommit/test/integ.codecommit-events.ts +++ b/packages/@aws-cdk/aws-codecommit/test/integ.codecommit-events.ts @@ -11,10 +11,12 @@ const topic = new sns.Topic(stack, 'MyTopic'); // we can't use @aws-cdk/aws-events-targets.SnsTopic here because it will // create a cyclic dependency with codebuild, so we just fake it repo.onReferenceCreated('OnReferenceCreated', { - bind: () => ({ - arn: topic.topicArn, - id: 'MyTopic' - }) + target: { + bind: () => ({ + arn: topic.topicArn, + id: 'MyTopic' + }) + } }); app.run(); diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts index 379fcfa2b57cf..eae6b45745927 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts @@ -170,10 +170,10 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy this.deploymentGroupArn = arnForDeploymentGroup(this.application.applicationName, this.deploymentGroupName); if (props.preHook) { - this.onPreHook(props.preHook); + this.addPreHook(props.preHook); } if (props.postHook) { - this.onPostHook(props.postHook); + this.addPostHook(props.postHook); } (props.alias.node.findChild('Resource') as lambda.CfnAlias).options.updatePolicy = { @@ -200,7 +200,7 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy * @param preHook function to run before deployment beings * @throws an error if a pre-hook function is already configured */ - public onPreHook(preHook: lambda.IFunction): void { + public addPreHook(preHook: lambda.IFunction): void { if (this.preHook !== undefined) { throw new Error('A pre-hook function is already defined for this deployment group'); } @@ -214,7 +214,7 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy * @param postHook function to run after deployment completes * @throws an error if a post-hook function is already configured */ - public onPostHook(postHook: lambda.IFunction): void { + public addPostHook(postHook: lambda.IFunction): void { if (this.postHook !== undefined) { throw new Error('A post-hook function is already defined for this deployment group'); } diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts index 3a13c87e03121..ffe617c10d918 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.deployment-group.ts @@ -227,7 +227,7 @@ export = { preHook: mockFunction(stack, 'PreHook'), deploymentConfig: LambdaDeploymentConfig.AllAtOnce }); - test.throws(() => group.onPreHook(mockFunction(stack, 'PreHook2'))); + test.throws(() => group.addPreHook(mockFunction(stack, 'PreHook2'))); test.done(); }, "onPostHook throws error if post-hook already defined"(test: Test) { @@ -238,7 +238,7 @@ export = { postHook: mockFunction(stack, 'PostHook'), deploymentConfig: LambdaDeploymentConfig.AllAtOnce }); - test.throws(() => group.onPostHook(mockFunction(stack, 'PostHook2'))); + test.throws(() => group.addPostHook(mockFunction(stack, 'PostHook2'))); test.done(); }, "can run pre hook lambda function before deployment"(test: Test) { @@ -299,7 +299,7 @@ export = { alias, deploymentConfig: LambdaDeploymentConfig.AllAtOnce }); - group.onPreHook(mockFunction(stack, 'PreHook')); + group.addPreHook(mockFunction(stack, 'PreHook')); expect(stack).to(haveResourceLike('AWS::Lambda::Alias', { UpdatePolicy: { @@ -397,7 +397,7 @@ export = { alias, deploymentConfig: LambdaDeploymentConfig.AllAtOnce }); - group.onPostHook(mockFunction(stack, 'PostHook')); + group.addPostHook(mockFunction(stack, 'PostHook')); expect(stack).to(haveResourceLike('AWS::Lambda::Alias', { UpdatePolicy: { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts index 741a1478446ce..3e96e368e710b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts @@ -57,8 +57,10 @@ export class CodeCommitSourceAction extends codepipeline.Action { protected bind(info: codepipeline.ActionBind): void { if (!this.props.pollForSourceChanges) { - this.props.repository.onCommit(info.pipeline.node.uniqueId + 'EventRule', - new targets.CodePipeline(info.pipeline), this.props.branch || 'master'); + this.props.repository.onCommit(info.pipeline.node.uniqueId + 'EventRule', { + target: new targets.CodePipeline(info.pipeline), + branches: [this.props.branch || 'master'] + }); } // https://docs.aws.amazon.com/codecommit/latest/userguide/auth-and-access-control-permissions-reference.html#aa-acp diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts index e86c3bed59739..04f927719089e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts @@ -28,6 +28,10 @@ export interface EcrSourceActionProps extends codepipeline.CommonActionProps { /** * The ECR Repository source CodePipeline Action. + * + * Will trigger the pipeline as soon as the target tag in the repository + * changes, but only if there is a CloudTrail Trail in the account that + * captures the ECR event. */ export class EcrSourceAction extends codepipeline.Action { private readonly props: EcrSourceActionProps; @@ -55,7 +59,9 @@ export class EcrSourceAction extends codepipeline.Action { ) .addResource(this.props.repository.repositoryArn)); - this.props.repository.onImagePushed(info.pipeline.node.uniqueId + 'SourceEventRule', - new targets.CodePipeline(info.pipeline), this.props.imageTag); + this.props.repository.onCloudTrailImagePushed(info.pipeline.node.uniqueId + 'SourceEventRule', { + target: new targets.CodePipeline(info.pipeline), + imageTag: this.props.imageTag + }); } } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts index 88f003dd29732..eb4065cb0f3ff 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts @@ -38,6 +38,9 @@ export interface S3SourceActionProps extends codepipeline.CommonActionProps { /** * Source that is provided by a specific Amazon S3 object. + * + * Will trigger the pipeline as soon as the S3 object changes, but only if there is + * a CloudTrail Trail in the account that captures the S3 event. */ export class S3SourceAction extends codepipeline.Action { private readonly props: S3SourceActionProps; @@ -61,8 +64,10 @@ export class S3SourceAction extends codepipeline.Action { protected bind(info: codepipeline.ActionBind): void { if (this.props.pollForSourceChanges === false) { - this.props.bucket.onPutObject(info.pipeline.node.uniqueId + 'SourceEventRule', - new targets.CodePipeline(info.pipeline), this.props.bucketKey); + this.props.bucket.onCloudTrailPutObject(info.pipeline.node.uniqueId + 'SourceEventRule', { + target: new targets.CodePipeline(info.pipeline), + paths: [this.props.bucketKey] + }); } // pipeline needs permissions to read from the S3 bucket diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts index 656f96b26de24..028ed4a69fe57 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts @@ -324,6 +324,13 @@ class PipelineDouble extends cdk.Construct implements codepipeline.IPipeline { public grantBucketReadWrite(_identity?: iam.IGrantable): iam.Grant { throw new Error('grantBucketReadWrite() is unsupported in PipelineDouble'); } + + public onEvent(_id: string, _options: events.OnEventOptions): events.Rule { + throw new Error("Method not implemented."); + } + public onStateChange(_id: string, _options: events.OnEventOptions): events.Rule { + throw new Error("Method not implemented."); + } } class StageDouble implements codepipeline.IStage { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json index 66b99d6d54d05..6878eb270286e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json @@ -395,9 +395,6 @@ "AWS API Call via CloudTrail" ], "detail": { - "eventSource": [ - "s3.amazonaws.com" - ], "eventName": [ "PutObject" ], @@ -666,4 +663,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json index 9c3df28286ac0..f7d21ee67d544 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecr-source.expected.json @@ -235,6 +235,9 @@ "source": [ "aws.ecr" ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], "detail": { "eventName": [ "PutImage" @@ -286,4 +289,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.ts index 328bb6d6c293f..30b2ce6cb3b33 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.ts @@ -48,9 +48,11 @@ const topic = new sns.Topic(stack, 'MyTopic'); const eventPipeline = events.EventField.fromPath('$.detail.pipeline'); const eventState = events.EventField.fromPath('$.detail.state'); -pipeline.onStateChange('OnPipelineStateChange').addTarget(new targets.SnsTopic(topic, { - message: events.RuleTargetInput.fromText(`Pipeline ${eventPipeline} changed state to ${eventState}`), -})); +pipeline.onStateChange('OnPipelineStateChange', { + target: new targets.SnsTopic(topic, { + message: events.RuleTargetInput.fromText(`Pipeline ${eventPipeline} changed state to ${eventState}`), + }) +}); sourceStage.onStateChange('OnSourceStateChange', new targets.SnsTopic(topic)); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts index afe45b320f0bc..3c4bb6a7fb70e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts @@ -310,9 +310,9 @@ export = { ], }); - pipeline.onStateChange('OnStateChange', new targets.SnsTopic(topic), { + pipeline.onStateChange('OnStateChange', { + target: new targets.SnsTopic(topic), description: 'desc', - scheduleExpression: 'now', eventPattern: { detail: { state: [ 'FAILED' ] @@ -360,7 +360,6 @@ export = { } ] }, - "ScheduleExpression": "now", "State": "ENABLED", "Targets": [ { diff --git a/packages/@aws-cdk/aws-codepipeline/lib/action.ts b/packages/@aws-cdk/aws-codepipeline/lib/action.ts index e8dabace20812..a63caa198c5a5 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/action.ts @@ -86,6 +86,23 @@ export interface IPipeline extends IResource { * @param identity the IAM Identity to grant the permissions to */ grantBucketReadWrite(identity: iam.IGrantable): iam.Grant; + + /** + * Define an event rule triggered by this CodePipeline. + * + * @param id Identifier for this event handler. + * @param options Additional options to pass to the event rule. + */ + onEvent(id: string, options: events.OnEventOptions): events.Rule; + + /** + * Define an event rule triggered by the "CodePipeline Pipeline Execution + * State Change" event emitted from this pipeline. + * + * @param id Identifier for this event handler. + * @param options Additional options to pass to the event rule. + */ + onStateChange(id: string, options: events.OnEventOptions): events.Rule; } /** diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 70e268badb771..03622bf4fe3e0 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -118,6 +118,37 @@ abstract class PipelineBase extends Resource implements IPipeline { public abstract grantBucketRead(identity: iam.IGrantable): iam.Grant; public abstract grantBucketReadWrite(identity: iam.IGrantable): iam.Grant; + /** + * Defines an event rule triggered by this CodePipeline. + * + * @param id Identifier for this event handler. + * @param options Additional options to pass to the event rule. + */ + public onEvent(id: string, options: events.OnEventOptions): events.Rule { + const rule = new events.Rule(this, id, options); + rule.addTarget(options.target); + rule.addEventPattern({ + source: [ 'aws.codepipeline' ], + resources: [ this.pipelineArn ], + }); + return rule; + } + + /** + * Defines an event rule triggered by the "CodePipeline Pipeline Execution + * State Change" event emitted from this pipeline. + * + * @param id Identifier for this event handler. + * @param options Additional options to pass to the event rule. + */ + public onStateChange(id: string, options: events.OnEventOptions): events.Rule { + const rule = this.onEvent(id, options); + rule.addEventPattern({ + detailType: [ 'CodePipeline Pipeline Execution State Change' ], + }); + return rule; + } + } /** @@ -280,31 +311,6 @@ export class Pipeline extends PipelineBase { this.role.addToPolicy(statement); } - /** - * Defines an event rule triggered by the "CodePipeline Pipeline Execution - * State Change" event emitted from this pipeline. - * - * @param target Initial target to add to the event rule. You can also add - * targets and customize target inputs by calling `rule.addTarget(target[, - * options])` after the rule was created. - * - * @param options Additional options to pass to the event rule - * - * @param name The name of the event rule construct. If you wish to define - * more than a single onStateChange event, you will need to explicitly - * specify a name. - */ - public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { - const rule = new events.Rule(this, name, options); - rule.addTarget(target); - rule.addEventPattern({ - detailType: [ 'CodePipeline Pipeline Execution State Change' ], - source: [ 'aws.codepipeline' ], - resources: [ this.pipelineArn ], - }); - return rule; - } - /** * Get the number of Stages in this Pipeline. */ diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 044bf87ec4693..ed478523bfa86 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -407,7 +407,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-create-auth-challenge.html * @param fn the lambda function to attach */ - public onCreateAuthChallenge(fn: lambda.IFunction): void { + public addCreateAuthChallengeTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'CreateAuthChallenge'); this.triggers = { ...this.triggers, createAuthChallenge: fn.functionArn }; } @@ -418,7 +418,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html * @param fn the lambda function to attach */ - public onCustomMessage(fn: lambda.IFunction): void { + public addCustomMessageTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'CustomMessage'); this.triggers = { ...this.triggers, customMessage: fn.functionArn }; } @@ -429,7 +429,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-define-auth-challenge.html * @param fn the lambda function to attach */ - public onDefineAuthChallenge(fn: lambda.IFunction): void { + public addDefineAuthChallengeTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'DefineAuthChallenge'); this.triggers = { ...this.triggers, defineAuthChallenge: fn.functionArn }; } @@ -440,7 +440,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-authentication.html * @param fn the lambda function to attach */ - public onPostAuthentication(fn: lambda.IFunction): void { + public addPostAuthenticationTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'PostAuthentication'); this.triggers = { ...this.triggers, postAuthentication: fn.functionArn }; } @@ -451,7 +451,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html * @param fn the lambda function to attach */ - public onPostConfirmation(fn: lambda.IFunction): void { + public addPostConfirmationTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'PostConfirmation'); this.triggers = { ...this.triggers, postConfirmation: fn.functionArn }; } @@ -462,7 +462,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-authentication.html * @param fn the lambda function to attach */ - public onPreAuthentication(fn: lambda.IFunction): void { + public addPreAuthenticationTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'PreAuthentication'); this.triggers = { ...this.triggers, preAuthentication: fn.functionArn }; } @@ -473,7 +473,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html * @param fn the lambda function to attach */ - public onPreSignUp(fn: lambda.IFunction): void { + public addPreSignUpTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'PreSignUp'); this.triggers = { ...this.triggers, preSignUp: fn.functionArn }; } @@ -484,7 +484,7 @@ export class UserPool extends Resource implements IUserPool { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-verify-auth-challenge-response.html * @param fn the lambda function to attach */ - public onVerifyAuthChallengeResponse(fn: lambda.IFunction): void { + public addVerifyAuthChallengeResponseTrigger(fn: lambda.IFunction): void { this.addLambdaPermission(fn, 'VerifyAuthChallengeResponse'); this.triggers = { ...this.triggers, verifyAuthChallengeResponse: fn.functionArn }; } diff --git a/packages/@aws-cdk/aws-cognito/test/test.user-pool.ts b/packages/@aws-cdk/aws-cognito/test/test.user-pool.ts index 10b2fa45711f8..fb8c330b7e1a8 100644 --- a/packages/@aws-cdk/aws-cognito/test/test.user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/test/test.user-pool.ts @@ -37,7 +37,7 @@ export = { preSignUp: fn } }); - pool.onCustomMessage(fn); + pool.addCustomMessageTrigger(fn); // THEN expect(stack).to(haveResourceLike('AWS::Cognito::UserPool', { @@ -61,14 +61,14 @@ export = { // WHEN const pool = new cognito.UserPool(stack, 'Pool', { }); - pool.onCreateAuthChallenge(fn); - pool.onCustomMessage(fn); - pool.onDefineAuthChallenge(fn); - pool.onPostAuthentication(fn); - pool.onPostConfirmation(fn); - pool.onPreAuthentication(fn); - pool.onPreSignUp(fn); - pool.onVerifyAuthChallengeResponse(fn); + pool.addCreateAuthChallengeTrigger(fn); + pool.addCustomMessageTrigger(fn); + pool.addDefineAuthChallengeTrigger(fn); + pool.addPostAuthenticationTrigger(fn); + pool.addPostConfirmationTrigger(fn); + pool.addPreAuthenticationTrigger(fn); + pool.addPreSignUpTrigger(fn); + pool.addVerifyAuthChallengeResponseTrigger(fn); // THEN expect(stack).to(haveResourceLike('AWS::Cognito::UserPool', { diff --git a/packages/@aws-cdk/aws-config/README.md b/packages/@aws-cdk/aws-config/README.md index 98c190ed0b719..d8e45ea18d38f 100644 --- a/packages/@aws-cdk/aws-config/README.md +++ b/packages/@aws-cdk/aws-config/README.md @@ -63,7 +63,9 @@ To define Amazon CloudWatch event rules, use the `onComplianceChange()` or `onRe ```ts const rule = new CloudFormationStackDriftDetectionCheck(this, 'Drift'); -rule.onComplianceChange(topic); +rule.onComplianceChange('TopicEvent', { + target: new targets.SnsTopic(topic)) +}); ``` #### Example diff --git a/packages/@aws-cdk/aws-config/lib/rule.ts b/packages/@aws-cdk/aws-config/lib/rule.ts index 3d9b742763d88..1ee8783db6f19 100644 --- a/packages/@aws-cdk/aws-config/lib/rule.ts +++ b/packages/@aws-cdk/aws-config/lib/rule.ts @@ -19,17 +19,17 @@ export interface IRule extends IResource { * Defines a CloudWatch event rule which triggers for rule events. Use * `rule.addEventPattern(pattern)` to specify a filter. */ - onEvent(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onEvent(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers for rule compliance events. */ - onComplianceChange(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onComplianceChange(id: string, options: events.OnEventOptions): events.Rule; /** * Defines a CloudWatch event rule which triggers for rule re-evaluation status events. */ - onReEvaluationStatus(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + onReEvaluationStatus(id: string, options: events.OnEventOptions): events.Rule; } /** @@ -65,7 +65,7 @@ abstract class RuleBase extends Resource implements IRule { * Defines a CloudWatch event rule which triggers for rule events. Use * `rule.addEventPattern(pattern)` to specify a filter. */ - public onEvent(id: string, target?: events.IRuleTarget, options?: events.RuleProps) { + public onEvent(id: string, options: events.OnEventOptions) { const rule = new events.Rule(this, id, options); rule.addEventPattern({ source: ['aws.config'], @@ -73,15 +73,15 @@ abstract class RuleBase extends Resource implements IRule { configRuleName: [this.configRuleName] } }); - rule.addTarget(target); + rule.addTarget(options.target); return rule; } /** * Defines a CloudWatch event rule which triggers for rule compliance events. */ - public onComplianceChange(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { - const rule = this.onEvent(id, target, options); + public onComplianceChange(id: string, options: events.OnEventOptions): events.Rule { + const rule = this.onEvent(id, options); rule.addEventPattern({ detailType: [ 'Config Rules Compliance Change' ], }); @@ -91,8 +91,8 @@ abstract class RuleBase extends Resource implements IRule { /** * Defines a CloudWatch event rule which triggers for rule re-evaluation status events. */ - public onReEvaluationStatus(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { - const rule = this.onEvent(id, target, options); + public onReEvaluationStatus(id: string, options: events.OnEventOptions): events.Rule { + const rule = this.onEvent(id, options); rule.addEventPattern({ detailType: [ 'Config Rules Re-evaluation Status' ], }); diff --git a/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts b/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts index db58cda3f4979..0dc7480879ee0 100644 --- a/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts +++ b/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts @@ -32,7 +32,9 @@ class ConfigStack extends cdk.Stack { const complianceTopic = new sns.Topic(this, 'ComplianceTopic'); // Send notification on compliance change - driftRule.onComplianceChange('ComplianceChange', new targets.SnsTopic(complianceTopic)); + driftRule.onComplianceChange('ComplianceChange', { + target: new targets.SnsTopic(complianceTopic) + }); /// !hide } } diff --git a/packages/@aws-cdk/aws-config/test/test.rule.ts b/packages/@aws-cdk/aws-config/test/test.rule.ts index 38d8c9b2099f0..464a6960414b6 100644 --- a/packages/@aws-cdk/aws-config/test/test.rule.ts +++ b/packages/@aws-cdk/aws-config/test/test.rule.ts @@ -256,7 +256,9 @@ export = { }); // WHEN - rule.onComplianceChange('ComplianceChange', new targets.LambdaFunction(fn)); + rule.onComplianceChange('ComplianceChange', { + target: new targets.LambdaFunction(fn) + }); expect(stack).to(haveResource('AWS::Events::Rule', { EventPattern: { diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 658c87c15a876..1c8afe1a47171 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -58,14 +58,28 @@ export interface IRepository extends IResource { */ grantPullPush(grantee: iam.IGrantable): iam.Grant; + /** + * Define a CloudWatch event that triggers when something happens to this repository + * + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule + */ + onCloudTrailEvent(id: string, options: events.OnEventOptions): events.Rule; + /** * Defines an AWS CloudWatch event rule that can trigger a target when an image is pushed to this * repository. - * @param name The name of the rule - * @param target An IRuleTarget to invoke when this event happens (you can add more targets using `addTarget`) - * @param imageTag Only trigger on the specific image tag + * + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule */ - onImagePushed(name: string, target?: events.IRuleTarget, imageTag?: string): events.Rule; + onCloudTrailImagePushed(id: string, options: OnCloudTrailImagePushedOptions): events.Rule; } /** @@ -110,31 +124,51 @@ export abstract class RepositoryBase extends Resource implements IRepository { return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${this.repositoryName}${tagSuffix}`; } + /** + * Define a CloudWatch event that triggers when something happens to this repository + * + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule + */ + public onCloudTrailEvent(id: string, options: events.OnEventOptions): events.Rule { + const rule = new events.Rule(this, id, options); + rule.addTarget(options.target); + rule.addEventPattern({ + source: ['aws.ecr'], + detailType: ['AWS API Call via CloudTrail'], + detail: { + requestParameters: { + repositoryName: [this.repositoryName], + } + } + }); + return rule; + } + /** * Defines an AWS CloudWatch event rule that can trigger a target when an image is pushed to this * repository. - * @param name The name of the rule - * @param target An IRuleTarget to invoke when this event happens (you can add more targets using `addTarget`) - * @param imageTag Only trigger on the specific image tag + * + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule */ - public onImagePushed(name: string, target?: events.IRuleTarget, imageTag?: string): events.Rule { - return new events.Rule(this, name, { - targets: target ? [target] : undefined, - eventPattern: { - source: ['aws.ecr'], - detail: { - eventName: [ - 'PutImage', - ], - requestParameters: { - repositoryName: [ - this.repositoryName, - ], - imageTag: imageTag ? [imageTag] : undefined, - }, + public onCloudTrailImagePushed(id: string, options: OnCloudTrailImagePushedOptions): events.Rule { + const rule = this.onCloudTrailEvent(id, options); + rule.addEventPattern({ + detail: { + eventName: ['PutImage'], + requestParameters: { + imageTag: options.imageTag ? [options.imageTag] : undefined, }, }, }); + return rule; } /** @@ -178,6 +212,18 @@ export abstract class RepositoryBase extends Resource implements IRepository { } } +/** + * Options for the onCloudTrailImagePushed method + */ +export interface OnCloudTrailImagePushedOptions extends events.OnEventOptions { + /** + * Only watch changes to this image tag + * + * @default - Watch changes to all tags + */ + readonly imageTag?: string; +} + export interface RepositoryProps { /** * Name for this repository diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index e83eeefd26704..76b9865efae30 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -285,11 +285,15 @@ export = { }, 'events': { - 'onImagePushed without target or imageTag creates the correct event'(test: Test) { + 'onImagePushed without imageTag creates the correct event'(test: Test) { const stack = new cdk.Stack(); const repo = new ecr.Repository(stack, 'Repo'); - repo.onImagePushed('EventRule'); + repo.onCloudTrailImagePushed('EventRule', { + target: { + bind: () => ({ arn: 'ARN', id: 'ID' }) + } + }); expect(stack).to(haveResourceLike('AWS::Events::Rule', { "EventPattern": { @@ -303,7 +307,8 @@ export = { "requestParameters": { "repositoryName": [ { - }, + "Ref": "Repo02AC86CF" + } ], }, }, diff --git a/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts b/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts index 9605f66bf0af9..c5eecb95735f2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts +++ b/packages/@aws-cdk/aws-ecs/lib/drain-hook/instance-drain-hook.ts @@ -63,7 +63,7 @@ export class InstanceDrainHook extends cdk.Construct { }); // Hook everything up: ASG -> Topic, Topic -> Lambda - props.autoScalingGroup.onLifecycleTransition('DrainHook', { + props.autoScalingGroup.addLifecycleHook('DrainHook', { lifecycleTransition: autoscaling.LifecycleTransition.InstanceTerminating, defaultResult: autoscaling.DefaultResult.Continue, notificationTarget: topic, diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts index 357a3c592ffe0..06b0231f42b6e 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts @@ -23,17 +23,23 @@ topic.subscribeQueue(queue); // this will send an email with the JSON event for every state change of this // build project. -project.onStateChange('StateChange', new targets.SnsTopic(topic)); +project.onStateChange('StateChange', { target: new targets.SnsTopic(topic) }); // this will send an email with the message "Build phase changed to ". // The phase will be extracted from the "completed-phase" field of the event // details. -project.onPhaseChange('PhaseChange').addTarget(new targets.SnsTopic(topic, { - message: events.RuleTargetInput.fromText(`Build phase changed to ${codebuild.PhaseChangeEvent.completedPhase}`) -})); +project.onPhaseChange('PhaseChange', { + target: new targets.SnsTopic(topic, { + message: events.RuleTargetInput.fromText(`Build phase changed to ${codebuild.PhaseChangeEvent.completedPhase}`) + }) +}); // trigger a build when a commit is pushed to the repo -const onCommitRule = repo.onCommit('OnCommit', new targets.CodeBuildProject(project), 'master'); +const onCommitRule = repo.onCommit('OnCommit', { + target: new targets.CodeBuildProject(project), + branches: ['master'] +}); + onCommitRule.addTarget(new targets.SnsTopic(topic, { message: events.RuleTargetInput.fromText( `A commit was pushed to the repository ${codecommit.ReferenceEvent.repositoryName} on branch ${codecommit.ReferenceEvent.referenceName}` diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json new file mode 100644 index 0000000000000..5c4b2bf89391c --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json @@ -0,0 +1,380 @@ +{ + "Resources": { + "Repo02AC86CF": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "TestRepository", + "Triggers": [] + } + }, + "PipelineArtifactsBucketEncryptionKey01D58D69": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + } + }, + "DeletionPolicy": "Retain" + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "codepipeline.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Name" + ] + }, + "BranchName": "master" + }, + "InputArtifacts": [], + "Name": "CodeCommit", + "OutputArtifacts": [ + { + "Name": "Src" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "InputArtifacts": [], + "Name": "Hello", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "Build" + } + ], + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + }, + "Type": "KMS" + }, + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleDefaultPolicyC7A05455", + "PipelineRoleD68726F7" + ] + }, + "PipelineEventsRole46BEEA7C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "events.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "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" + } + ] + } + }, + "ruleF2C1DCDC": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "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" + ] + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts new file mode 100644 index 0000000000000..36c15e999eedf --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts @@ -0,0 +1,50 @@ +import codecommit = require('@aws-cdk/aws-codecommit'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import events = require('@aws-cdk/aws-events'); +import cdk = require('@aws-cdk/cdk'); +import targets = require('../../lib'); + +class MockAction extends codepipeline.Action { + protected bind(_info: codepipeline.ActionBind): void { + // void + } +} + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'pipeline-events'); + +const repo = new codecommit.Repository(stack, 'Repo', { + repositoryName: 'TestRepository' +}); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + +const srcArtifact = new codepipeline.Artifact('Src'); +pipeline.addStage({ + name: 'Source', + actions: [new MockAction({ + actionName: 'CodeCommit', + category: codepipeline.ActionCategory.Source, + provider: 'CodeCommit', + artifactBounds: { minInputs: 0, maxInputs: 0 , minOutputs: 1, maxOutputs: 1, }, + configuration: { + RepositoryName: repo.repositoryName, + BranchName: 'master', + }, + outputs: [srcArtifact]})] +}); +pipeline.addStage({ + name: 'Build', + actions: [new MockAction({ + actionName: 'Hello', + category: codepipeline.ActionCategory.Approval, + provider: 'Manual', + artifactBounds: { minInputs: 0, maxInputs: 0 , minOutputs: 0, maxOutputs: 0, }})] +}); + +new events.Rule(stack, 'rule', { + scheduleExpression: 'rate(1 minute)', + targets: [new targets.CodePipeline(pipeline)] +}); + +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/lib/index.ts b/packages/@aws-cdk/aws-events/lib/index.ts index 81e2633f09a6f..0409be7ae29d5 100644 --- a/packages/@aws-cdk/aws-events/lib/index.ts +++ b/packages/@aws-cdk/aws-events/lib/index.ts @@ -3,6 +3,7 @@ export * from './rule'; export * from './rule-ref'; export * from './target'; export * from './event-pattern'; +export * from './on-event-options'; // AWS::Events CloudFormation Resources: export * from './events.generated'; diff --git a/packages/@aws-cdk/aws-events/lib/on-event-options.ts b/packages/@aws-cdk/aws-events/lib/on-event-options.ts new file mode 100644 index 0000000000000..047722cb35ab7 --- /dev/null +++ b/packages/@aws-cdk/aws-events/lib/on-event-options.ts @@ -0,0 +1,36 @@ +import { EventPattern } from "./event-pattern"; +import { IRuleTarget } from "./target"; + +/** + * Standard set of options for `onXxx` event handlers on construct + */ +export interface OnEventOptions { + /** + * The target to register for the event + */ + readonly target: IRuleTarget; + + /** + * A description of the rule's purpose. + */ + readonly description?: string; + + /** + * A name for the rule. + * + * @default AWS CloudFormation generates a unique physical ID. + */ + readonly ruleName?: string; + + /** + * Additional restrictions for the event to route to the specified target + * + * The method that generates the rule probably imposes some type of event + * filtering. The filtering implied by what you pass here is added + * on top of that filtering. + * + * @see + * http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CloudWatchEventsandEventPatterns.html + */ + readonly eventPattern?: EventPattern; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/lib/util.ts b/packages/@aws-cdk/aws-events/lib/util.ts index 99b26f9e19493..1b1c860d7850d 100644 --- a/packages/@aws-cdk/aws-events/lib/util.ts +++ b/packages/@aws-cdk/aws-events/lib/util.ts @@ -21,12 +21,14 @@ export function mergeEventPattern(dest: any, src: any) { const srcValue = srcObj[field]; const destValue = destObj[field]; + if (srcValue === undefined) { continue; } + if (typeof(srcValue) !== 'object') { throw new Error(`Invalid event pattern field { ${field}: ${JSON.stringify(srcValue)} }. All fields must be arrays`); } // dest doesn't have this field - if (!(field in destObj)) { + if (destObj[field] === undefined) { destObj[field] = srcValue; continue; } diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts index 3b669ad26a0f2..7bb2726f2b4ff 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts @@ -27,7 +27,7 @@ export class S3EventSource implements lambda.IEventSource { public bind(target: lambda.IFunction) { const filters = this.props.filters || []; for (const event of this.props.events) { - this.bucket.onEvent(event, target, ...filters); + this.bucket.addEventNotification(event, target, ...filters); } } } diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.ts b/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.ts index eeb14db4809f1..663175eec774a 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.bucket-notifications.ts @@ -20,8 +20,8 @@ const bucketB = new s3.Bucket(stack, 'YourBucket', { removalPolicy: cdk.RemovalPolicy.Destroy }); -bucketA.onObjectCreated(fn, { suffix: '.png' }); -bucketB.onEvent(s3.EventType.ObjectRemoved, fn); +bucketA.addObjectCreatedNotification(fn, { suffix: '.png' }); +bucketB.addEventNotification(s3.EventType.ObjectRemoved, fn); app.run(); diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 9166020e24a43..9da3738645841 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -166,14 +166,27 @@ export interface IBucket extends IResource { grantPublicAccess(keyPrefix?: string, ...allowedActions: string[]): iam.Grant; /** - * Defines a CloudWatch Event Rule that triggers upon putting an object into the Bucket. + * Define a CloudWatch event that triggers when something happens to this repository * - * @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.Rule} instance + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule + */ + onCloudTrailEvent(id: string, options: OnCloudTrailBucketEventOptions): events.Rule; + + /** + * Defines an AWS CloudWatch event rule that can trigger a target when an image is pushed to this + * repository. + * + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule */ - onPutObject(name: string, target?: events.IRuleTarget, path?: string): events.Rule; + onCloudTrailPutObject(id: string, options: OnCloudTrailBucketEventOptions): events.Rule; } /** @@ -278,32 +291,48 @@ abstract class BucketBase extends Resource implements IBucket { */ protected abstract disallowPublicAccess?: boolean; - public onPutObject(name: string, target?: events.IRuleTarget, path?: string): events.Rule { - const eventRule = new events.Rule(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, - ], - }, + /** + * Define a CloudWatch event that triggers when something happens to this repository + * + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule + */ + public onCloudTrailEvent(id: string, options: OnCloudTrailBucketEventOptions): events.Rule { + const rule = new events.Rule(this, id, options); + rule.addTarget(options.target); + rule.addEventPattern({ + source: ['aws.s3'], + detailType: ['AWS API Call via CloudTrail'], + detail: { + resources: { + ARN: options.paths ? options.paths.map(p => this.arnForObjects(p)) : [this.bucketArn], }, + } + }); + return rule; + } + + /** + * Defines an AWS CloudWatch event rule that can trigger a target when an image is pushed to this + * repository. + * + * Requires that there exists at least one CloudTrail Trail in your account + * that captures the event. This method will not create the Trail. + * + * @param id The id of the rule + * @param options Options for adding the rule + */ + public onCloudTrailPutObject(id: string, options: OnCloudTrailBucketEventOptions): events.Rule { + const rule = this.onCloudTrailEvent(id, options); + rule.addEventPattern({ + detail: { + eventName: ['PutObject'], }, }); - eventRule.addTarget(target); - return eventRule; + return rule; } /** @@ -829,12 +858,12 @@ export class Bucket extends BucketBase { * * @example * - * bucket.onEvent(EventType.OnObjectCreated, myLambda, 'home/myusername/*') + * bucket.addEventNotification(EventType.OnObjectCreated, myLambda, 'home/myusername/*') * * @see * https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html */ - public onEvent(event: EventType, dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { + public addEventNotification(event: EventType, dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { this.notifications.addNotification(event, dest, ...filters); } @@ -846,8 +875,8 @@ export class Bucket extends BucketBase { * @param dest The notification destination (see onEvent) * @param filters Filters (see onEvent) */ - public onObjectCreated(dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { - return this.onEvent(EventType.ObjectCreated, dest, ...filters); + public addObjectCreatedNotification(dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { + return this.addEventNotification(EventType.ObjectCreated, dest, ...filters); } /** @@ -858,8 +887,8 @@ export class Bucket extends BucketBase { * @param dest The notification destination (see onEvent) * @param filters Filters (see onEvent) */ - public onObjectRemoved(dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { - return this.onEvent(EventType.ObjectRemoved, dest, ...filters); + public addObjectRemovedNotification(dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { + return this.addEventNotification(EventType.ObjectRemoved, dest, ...filters); } private validateBucketName(bucketName: string) { @@ -1181,3 +1210,15 @@ export interface NotificationKeyFilter { */ readonly suffix?: string; } + +/** + * Options for the onCloudTrailPutObject method + */ +export interface OnCloudTrailBucketEventOptions extends events.OnEventOptions { + /** + * Only watch changes to these object paths + * + * @default - Watch changes to all objects + */ + readonly paths?: string[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.notifications.ts b/packages/@aws-cdk/aws-s3/test/integ.notifications.ts index cdc989fe4e90a..67e5714ce7939 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.notifications.ts +++ b/packages/@aws-cdk/aws-s3/test/integ.notifications.ts @@ -13,12 +13,12 @@ const bucket = new s3.Bucket(stack, 'Bucket', { const topic = new Topic(stack, 'Topic'); const topic3 = new Topic(stack, 'Topic3'); -bucket.onEvent(s3.EventType.ObjectCreatedPut, topic); -bucket.onEvent(s3.EventType.ObjectRemoved, topic3, { prefix: 'home/myusername/' }); +bucket.addEventNotification(s3.EventType.ObjectCreatedPut, topic); +bucket.addEventNotification(s3.EventType.ObjectRemoved, topic3, { prefix: 'home/myusername/' }); const bucket2 = new s3.Bucket(stack, 'Bucket2', { removalPolicy: cdk.RemovalPolicy.Destroy }); -bucket2.onObjectRemoved(topic3, { prefix: 'foo' }, { suffix: 'foo/bar' }); +bucket2.addObjectRemovedNotification(topic3, { prefix: 'foo' }, { suffix: 'foo/bar' }); app.run(); diff --git a/packages/@aws-cdk/aws-s3/test/test.notifications.ts b/packages/@aws-cdk/aws-s3/test/test.notifications.ts index c495f3e7f3aee..74e35aac97091 100644 --- a/packages/@aws-cdk/aws-s3/test/test.notifications.ts +++ b/packages/@aws-cdk/aws-s3/test/test.notifications.ts @@ -34,7 +34,7 @@ export = { const topic = new Topic(stack, 'MyTopic'); - bucket.onEvent(s3.EventType.ObjectCreated, topic); + bucket.addEventNotification(s3.EventType.ObjectCreated, topic); expect(stack).to(haveResource('AWS::S3::Bucket')); expect(stack).to(haveResource('AWS::Lambda::Function', { Description: 'AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3)' })); @@ -50,7 +50,7 @@ export = { const topic = new Topic(stack, 'MyTopic'); - bucket.onEvent(s3.EventType.ObjectCreated, topic); + bucket.addEventNotification(s3.EventType.ObjectCreated, topic); expect(stack).to(haveResource('AWS::S3::Bucket')); expect(stack).to(haveResource('AWS::Lambda::Function', { @@ -70,7 +70,7 @@ export = { const topic = new Topic(stack, 'Topic'); const bucket = new s3.Bucket(stack, 'MyBucket'); - bucket.onObjectCreated(topic); + bucket.addObjectCreatedNotification(topic); expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { "Topics": [ @@ -135,9 +135,9 @@ export = { }) }; - bucket.onEvent(s3.EventType.ObjectCreated, queueTarget); - bucket.onEvent(s3.EventType.ObjectCreated, lambdaTarget); - bucket.onObjectRemoved(topicTarget, { prefix: 'prefix' }); + bucket.addEventNotification(s3.EventType.ObjectCreated, queueTarget); + bucket.addEventNotification(s3.EventType.ObjectCreated, lambdaTarget); + bucket.addObjectRemovedNotification(topicTarget, { prefix: 'prefix' }); expect(stack).to(haveResource('Custom::S3BucketNotifications', { "ServiceToken": { @@ -195,14 +195,14 @@ export = { const bucket = new s3.Bucket(stack, 'TestBucket'); - bucket.onEvent(s3.EventType.ObjectRemovedDelete, { + bucket.addEventNotification(s3.EventType.ObjectRemovedDelete, { asBucketNotificationDestination: _ => ({ type: s3n.BucketNotificationDestinationType.Queue, arn: 'arn:aws:sqs:...:queue1' }) }); - bucket.onEvent(s3.EventType.ObjectRemovedDelete, { + bucket.addEventNotification(s3.EventType.ObjectRemovedDelete, { asBucketNotificationDestination: _ => ({ type: s3n.BucketNotificationDestinationType.Queue, arn: 'arn:aws:sqs:...:queue2' @@ -250,7 +250,7 @@ export = { arn: 'arn:aws:sqs:...' }; - bucket.onEvent(s3.EventType.ObjectRemovedDelete, { asBucketNotificationDestination: _ => bucketNotificationTarget }, { prefix: 'images/', suffix: '.jpg' }); + bucket.addEventNotification(s3.EventType.ObjectRemovedDelete, { asBucketNotificationDestination: _ => bucketNotificationTarget }, { prefix: 'images/', suffix: '.jpg' }); expect(stack).to(haveResource('Custom::S3BucketNotifications', { "ServiceToken": { @@ -304,7 +304,7 @@ export = { }) }; - bucket.onObjectCreated(dest); + bucket.addObjectCreatedNotification(dest); stack.node.prepareTree(); test.deepEqual(SynthUtils.toCloudFormation(stack).Resources.BucketNotifications8F2E257D, { @@ -326,7 +326,11 @@ export = { const bucket = s3.Bucket.fromBucketAttributes(stack, 'Bucket', { bucketName: 'MyBucket', }); - bucket.onPutObject('PutRule'); + bucket.onCloudTrailPutObject('PutRule', { + target: { + bind: () => ({ arn: 'ARN', id: 'ID' }) + } + }); expect(stack).to(haveResourceLike('AWS::Events::Rule', { "EventPattern": { @@ -334,9 +338,6 @@ export = { "aws.s3", ], "detail": { - "eventSource": [ - "s3.amazonaws.com", - ], "eventName": [ "PutObject", ], @@ -369,7 +370,12 @@ export = { const bucket = s3.Bucket.fromBucketAttributes(stack, 'Bucket', { bucketName: 'MyBucket', }); - bucket.onPutObject('PutRule', undefined, 'my/path.zip'); + bucket.onCloudTrailPutObject('PutRule', { + target: { + bind: () => ({ arn: 'ARN', id: 'ID' }) + }, + paths: ['my/path.zip'] + }); expect(stack).to(haveResourceLike('AWS::Events::Rule', { "EventPattern": { @@ -377,9 +383,6 @@ export = { "aws.s3", ], "detail": { - "eventSource": [ - "s3.amazonaws.com", - ], "eventName": [ "PutObject", ], 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 index 1a702b4b89d52..db1527730f93a 100644 --- a/packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.ts +++ b/packages/@aws-cdk/aws-sns/test/integ.sns-bucket-notifications.ts @@ -12,8 +12,8 @@ class MyStack extends cdk.Stack { removalPolicy: cdk.RemovalPolicy.Destroy }); - bucket.onObjectCreated(objectCreateTopic); - bucket.onObjectRemoved(objectRemovedTopic, { prefix: 'foo/', suffix: '.txt' }); + bucket.addObjectCreatedNotification(objectCreateTopic); + bucket.addObjectRemovedNotification(objectRemovedTopic, { prefix: 'foo/', suffix: '.txt' }); } } diff --git a/packages/@aws-cdk/aws-sqs/test/integ.bucket-notifications.ts b/packages/@aws-cdk/aws-sqs/test/integ.bucket-notifications.ts index 8310efec49c50..e807defac5165 100644 --- a/packages/@aws-cdk/aws-sqs/test/integ.bucket-notifications.ts +++ b/packages/@aws-cdk/aws-sqs/test/integ.bucket-notifications.ts @@ -11,14 +11,14 @@ const bucket1 = new s3.Bucket(stack, 'Bucket1', { }); const queue = new sqs.Queue(stack, 'MyQueue'); -bucket1.onObjectCreated(queue); +bucket1.addObjectCreatedNotification(queue); const bucket2 = new s3.Bucket(stack, 'Bucket2', { removalPolicy: cdk.RemovalPolicy.Destroy }); -bucket2.onObjectCreated(queue, { suffix: '.png' }); +bucket2.addObjectCreatedNotification(queue, { suffix: '.png' }); const encryptedQueue = new sqs.Queue(stack, 'EncryptedQueue', { encryption: sqs.QueueEncryption.Kms }); -bucket1.onObjectRemoved(encryptedQueue); +bucket1.addObjectRemovedNotification(encryptedQueue); app.run(); diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index 12b230a283a94..fe0c6fd0fc7e2 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -240,7 +240,7 @@ export = { const queue = new sqs.Queue(stack, 'Queue'); const bucket = new s3.Bucket(stack, 'Bucket'); - bucket.onObjectRemoved(queue); + bucket.addObjectRemovedNotification(queue); expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { "PolicyDocument": { @@ -314,7 +314,7 @@ export = { const bucket = new s3.Bucket(stack, 'Bucket'); const queue = new sqs.Queue(stack, 'Queue', { encryption: sqs.QueueEncryption.Kms }); - bucket.onObjectCreated(queue); + bucket.addObjectCreatedNotification(queue); expect(stack).to(haveResource('AWS::KMS::Key', { "KeyPolicy": { @@ -381,7 +381,7 @@ export = { const stack = new Stack(); const queue = new sqs.Queue(stack, 'Queue', { encryption: sqs.QueueEncryption.KmsManaged }); const bucket = new s3.Bucket(stack, 'Bucket'); - test.throws(() => bucket.onObjectRemoved(queue), 'Unable to add statement to IAM resource policy for KMS key: "alias/aws/sqs"'); + test.throws(() => bucket.addObjectRemovedNotification(queue), 'Unable to add statement to IAM resource policy for KMS key: "alias/aws/sqs"'); test.done(); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index 02158a9c443a0..5dce6925b53d7 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -188,7 +188,7 @@ export abstract class State extends cdk.Construct implements IChainable { * Register this state as part of the given graph * * Don't call this. It will be called automatically when you work - * states normally. + * with states normally. */ public bindToGraph(graph: StateGraph) { if (this.containingGraph === graph) { return; } @@ -199,7 +199,7 @@ export abstract class State extends cdk.Construct implements IChainable { } this.containingGraph = graph; - this.onBindToGraph(graph); + this.whenBoundToGraph(graph); for (const incoming of this.incomingStates) { incoming.bindToGraph(graph); @@ -349,7 +349,7 @@ export abstract class State extends cdk.Construct implements IChainable { * * Can be overridden by subclasses. */ - protected onBindToGraph(graph: StateGraph) { + protected whenBoundToGraph(graph: StateGraph) { graph.registerState(this); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index dd524d03fdbac..d0a0bbf820334 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -229,8 +229,8 @@ export class Task extends State implements INextable { return this.taskMetric(this.taskProps.metricPrefixPlural, 'HeartbeatTimedOut', props); } - protected onBindToGraph(graph: StateGraph) { - super.onBindToGraph(graph); + protected whenBoundToGraph(graph: StateGraph) { + super.whenBoundToGraph(graph); for (const policyStatement of this.taskProps.policyStatements || []) { graph.registerPolicyStatement(policyStatement); } diff --git a/tools/awslint/bin/awslint.ts b/tools/awslint/bin/awslint.ts index 236393580ce33..32a3c6c362363 100644 --- a/tools/awslint/bin/awslint.ts +++ b/tools/awslint/bin/awslint.ts @@ -5,7 +5,8 @@ import fs = require('fs-extra'); import reflect = require('jsii-reflect'); import path = require('path'); import yargs = require('yargs'); -import { AggregateLinter, apiLinter, attributesLinter, cfnResourceLinter, constructLinter, DiagnosticLevel, importsLinter, moduleLinter, resourceLinter, exportsLinter } from '../lib'; +import { AggregateLinter, apiLinter, attributesLinter, cfnResourceLinter, constructLinter, DiagnosticLevel, eventsLinter, exportsLinter, importsLinter, + moduleLinter, resourceLinter } from '../lib'; const linter = new AggregateLinter( moduleLinter, @@ -15,7 +16,8 @@ const linter = new AggregateLinter( apiLinter, importsLinter, attributesLinter, - exportsLinter + exportsLinter, + eventsLinter ); let stackTrace = false; diff --git a/tools/awslint/lib/linter.ts b/tools/awslint/lib/linter.ts index fa84010818b81..8ce53d42c1093 100644 --- a/tools/awslint/lib/linter.ts +++ b/tools/awslint/lib/linter.ts @@ -117,7 +117,6 @@ export class Evaluation { } public assert(condition: any, scope: string, extra?: string): condition is true { - // deduplicate: skip if this specific assertion ("rule:scope") was already examined if (this.diagnostics.find(d => d.rule.code === this.curr.code && d.scope === scope)) { return condition; @@ -126,11 +125,19 @@ export class Evaluation { const include = this.shouldEvaluate(this.curr.code, scope); const message = util.format(this.curr.message, extra || ''); + // Don't add a "Success" diagnostic. It will break if we run a compound + // linter rule which consists of 3 checks with the same scope (such + // as for example `assertSignature()`). If the first check fails, we would + // add a "Success" diagnostic and all other diagnostics would be skipped because + // of the deduplication check above. Changing the scope makes it worse, since + // the scope is also the ignore pattern and they're all conceptually the same rule. + // + // Simplest solution is to not record successes -- why do we even need them? + if (include && condition) { return condition; } + let level: DiagnosticLevel; if (!include) { level = DiagnosticLevel.Skipped; - } else if (condition) { - level = DiagnosticLevel.Success; } else if (this.curr.warning) { level = DiagnosticLevel.Warning; } else { @@ -159,6 +166,12 @@ export class Evaluation { return this.assert(a.toString() === e.toString(), scope, ` (expected="${e}",actual="${a}")`); } + public assertTypesAssignable(ts: reflect.TypeSystem, actual: TypeSpecifier, expected: TypeSpecifier, scope: string) { + const a = typeReferenceFrom(ts, actual); + const e = typeReferenceFrom(ts, expected); + return this.assert(a.toString() === e.toString() || (a.fqn && e.fqn && a.type!.extends(e.type!)), scope, ` ("${a}" not assignable to "${e}")`); + } + public assertSignature(method: reflect.Callable, expectations: MethodSignatureExpectations) { const scope = method.parentType.fqn + '.' + method.name; if (expectations.returns && reflect.Method.isMethod(method)) { @@ -179,7 +192,11 @@ export class Evaluation { this.assertEquals(actualName, expectedName, pscope); } if (expect.type) { - this.assertTypesEqual(method.system, actual.type, expect.type, pscope); + if (expect.subtypeAllowed) { + this.assertTypesAssignable(method.system, actual.type, expect.type, pscope); + } else { + this.assertTypesEqual(method.system, actual.type, expect.type, pscope); + } } } } @@ -254,6 +271,7 @@ export type TypeSpecifier = reflect.TypeReference | reflect.Type | string; export interface MethodSignatureParameterExpectation { name?: string; type?: TypeSpecifier; + subtypeAllowed?: boolean; /** should this param be optional? */ optional?: boolean; diff --git a/tools/awslint/lib/rules/cloudwatch-events.ts b/tools/awslint/lib/rules/cloudwatch-events.ts new file mode 100644 index 0000000000000..57dcbbb52923a --- /dev/null +++ b/tools/awslint/lib/rules/cloudwatch-events.ts @@ -0,0 +1,71 @@ +import reflect = require('jsii-reflect'); +import { Linter } from '../linter'; +import { ConstructReflection } from './construct'; +import { CoreTypes } from './core-types'; + +export const eventsLinter = new Linter(assembly => assembly.classes + .filter(t => CoreTypes.isConstructClass(t)) + .map(construct => new EventsReflection(construct))); + +export class EventsReflection extends ConstructReflection { + public get directEventMethods() { + return this.classType.allMethods.filter(isDirectEventMethod); + } + + public get cloudTrailEventMethods() { + return this.classType.allMethods.filter(isCloudTrailEventMethod); + } +} + +const ON_EVENT_OPTIONS_FQN = '@aws-cdk/aws-events.OnEventOptions'; +const EVENT_RULE_FQN = '@aws-cdk/aws-events.Rule'; + +eventsLinter.add({ + code: 'events-in-interface', + message: `'onXxx()' methods should also be defined on construct interface`, + eval: e => { + for (const method of e.ctx.directEventMethods.concat(e.ctx.cloudTrailEventMethods)) { + e.assert(!e.ctx.interfaceType || e.ctx.interfaceType.allMethods.some(m => m.name === method.name), `${e.ctx.fqn}.${method.name}`); + } + } +}); + +eventsLinter.add({ + code: 'events-generic', + message: `if there are specific 'onXxx()' methods, there should also be a generic 'onEvent()' method`, + eval: e => { + e.assert(e.ctx.directEventMethods.length === 0 || e.ctx.classType.allMethods.some(m => m.name === 'onEvent'), e.ctx.fqn); + } +}); + +eventsLinter.add({ + code: 'events-generic-cloudtrail', + message: `if there are specific 'onCloudTrailXxx()' methods, there should also be a generic 'onCloudTrailEvent()' method`, + eval: e => { + e.assert(e.ctx.cloudTrailEventMethods.length === 0 || e.ctx.classType.allMethods.some(m => m.name === 'onCloudTrailEvent'), e.ctx.fqn); + } +}); + +eventsLinter.add({ + code: 'events-method-signature', + message: `all 'onXxx()' methods should have the CloudWatch Events signature (id: string, options: events.OnEventOptions) => events.Rule`, + eval: e => { + for (const method of e.ctx.directEventMethods) { + e.assertSignature(method, { + parameters: [ + { type: 'string' }, + { type: ON_EVENT_OPTIONS_FQN, subtypeAllowed: true }, + ], + returns: EVENT_RULE_FQN + }); + } + } +}); + +function isDirectEventMethod(m: reflect.Method) { + return m.name.startsWith('on') && ! m.name.startsWith('onCloudTrail'); +} + +function isCloudTrailEventMethod(m: reflect.Method) { + return m.name.startsWith('onCloudTrail'); +} \ No newline at end of file diff --git a/tools/awslint/lib/rules/index.ts b/tools/awslint/lib/rules/index.ts index 2f8db16866e8e..31b837a8c9d62 100644 --- a/tools/awslint/lib/rules/index.ts +++ b/tools/awslint/lib/rules/index.ts @@ -6,3 +6,4 @@ export * from './cfn-resource'; export * from './attributes'; export * from './api'; export * from './exports'; +export * from './cloudwatch-events';