From 7112f840df509a735ea3f4dc2fb91731214e76dc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 8 May 2019 15:50:38 +0200 Subject: [PATCH 01/33] WIP --- packages/@aws-cdk/aws-ecs/README.md | 36 +++- packages/@aws-cdk/aws-ecs/package.json | 2 - .../lib/ecs-ec2-task.ts} | 0 .../test}/integ.event-task.lit.expected.json | 0 .../test}/integ.event-task.lit.ts | 0 .../test}/test.ec2-event-rule-target.ts | 0 .../test/test.target-inputs.ts | 124 ++++++++++++ packages/@aws-cdk/aws-events/lib/index.ts | 1 - .../@aws-cdk/aws-events/lib/input-options.ts | 52 ----- packages/@aws-cdk/aws-events/lib/rule.ts | 44 +---- packages/@aws-cdk/aws-events/lib/target.ts | 48 +++-- .../@aws-cdk/aws-events/test/test.rule.ts | 179 ++++-------------- 12 files changed, 237 insertions(+), 249 deletions(-) rename packages/@aws-cdk/{aws-ecs/lib/ec2/ec2-event-rule-target.ts => aws-events-targets/lib/ecs-ec2-task.ts} (100%) rename packages/@aws-cdk/{aws-ecs/test/ec2 => aws-events-targets/test}/integ.event-task.lit.expected.json (100%) rename packages/@aws-cdk/{aws-ecs/test/ec2 => aws-events-targets/test}/integ.event-task.lit.ts (100%) rename packages/@aws-cdk/{aws-ecs/test/ec2 => aws-events-targets/test}/test.ec2-event-rule-target.ts (100%) create mode 100644 packages/@aws-cdk/aws-events-targets/test/test.target-inputs.ts delete mode 100644 packages/@aws-cdk/aws-events/lib/input-options.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 6539c47812830..a6eed1991e3cf 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -287,7 +287,41 @@ you can configure on your instances. To start an Amazon ECS task on an Amazon EC2-backed Cluster, instantiate an `Ec2TaskEventRuleTarget` instead of an `Ec2Service`: -[example of CloudWatch Events integration](test/ec2/integ.event-task.lit.ts) +```ts +import targets = require('@aws-cdk/aws-events-targets'); + +// Create a Task Definition for the container to start +const taskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef'); +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromAsset(this, 'EventImage', { + directory: path.resolve(__dirname, '..', 'eventhandler-image') + }), + memoryLimitMiB: 256, + logging: new ecs.AwsLogDriver(this, 'TaskLogging', { streamPrefix: 'EventDemo' }) +}); + +// An EventRule that describes the event trigger (in this case a scheduled run) +const rule = new events.EventRule(this, 'Rule', { + scheduleExpression: 'rate(1 minute)', +}); + +// Use as the target of the EventRule +const target = new targets.Ec2EventRuleTarget(this, 'EventTarget', { + cluster, + taskDefinition, + taskCount: 1 +}); + +// Pass an environment variable to the container 'TheContainer' in the task +rule.addTarget(target, { + jsonTemplate: JSON.stringify({ + containerOverrides: [{ + name: 'TheContainer', + environment: [{ name: 'I_WAS_TRIGGERED', value: 'From CloudWatch Events' }] + }] + }) +}); +``` > Note: it is currently not possible to start AWS Fargate tasks in this way. diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index b2ee60994b16e..5fe12187092ed 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -77,7 +77,6 @@ "@aws-cdk/aws-ecr": "^0.31.0", "@aws-cdk/aws-elasticloadbalancing": "^0.31.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.31.0", - "@aws-cdk/aws-events": "^0.31.0", "@aws-cdk/aws-iam": "^0.31.0", "@aws-cdk/aws-lambda": "^0.31.0", "@aws-cdk/aws-logs": "^0.31.0", @@ -100,7 +99,6 @@ "@aws-cdk/aws-ecr": "^0.31.0", "@aws-cdk/aws-elasticloadbalancing": "^0.31.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.31.0", - "@aws-cdk/aws-events": "^0.31.0", "@aws-cdk/aws-iam": "^0.31.0", "@aws-cdk/aws-lambda": "^0.31.0", "@aws-cdk/aws-logs": "^0.31.0", diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-event-rule-target.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts similarity index 100% rename from packages/@aws-cdk/aws-ecs/lib/ec2/ec2-event-rule-target.ts rename to packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.expected.json similarity index 100% rename from packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json rename to packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.expected.json diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts b/packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.ts similarity index 100% rename from packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts rename to packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts b/packages/@aws-cdk/aws-events-targets/test/test.ec2-event-rule-target.ts similarity index 100% rename from packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts rename to packages/@aws-cdk/aws-events-targets/test/test.ec2-event-rule-target.ts diff --git a/packages/@aws-cdk/aws-events-targets/test/test.target-inputs.ts b/packages/@aws-cdk/aws-events-targets/test/test.target-inputs.ts new file mode 100644 index 0000000000000..1a85b998db3cf --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/test.target-inputs.ts @@ -0,0 +1,124 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import { EventRule, IEventRuleTarget } from '@aws-cdk/aws-events'; +import cdk = require('@aws-cdk/cdk'); +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; + +export = { + 'json template': { + 'can just be a JSON object'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + // WHEN + rule.addTarget(new SomeTarget(), { + jsonTemplate: { SomeObject: 'withAValue' }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "{\"SomeObject\":\"withAValue\"}" + }, + } + ] + })); + test.done(); + }, + }, + + 'text templates': { + 'strings with newlines are serialized to a newline-delimited list of JSON strings'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + // WHEN + rule.addTarget(new SomeTarget(), { + textTemplate: 'I have\nmultiple lines', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "\"I have\"\n\"multiple lines\"" + }, + } + ] + })); + + test.done(); + }, + + 'escaped newlines are not interpreted as newlines'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + // WHEN + rule.addTarget(new SomeTarget(), { + textTemplate: 'this is not\\na real newline', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "\"this is not\\\\na real newline\"" + }, + } + ] + })); + + test.done(); + }, + + 'can use Tokens in text templates'(test: Test) { + // GIVEN + const stack = new Stack(); + const rule = new EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + + const world = new cdk.Token(() => 'world'); + + // WHEN + rule.addTarget(new SomeTarget(), { + textTemplate: `hello ${world}`, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "\"hello world\"" + }, + } + ] + })); + + test.done(); + } + }, +}; + +class SomeTarget implements IEventRuleTarget { + public bind() { + return { + id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' } + }; + } +} \ 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 c2bd7a23dcb59..35093e32ed265 100644 --- a/packages/@aws-cdk/aws-events/lib/index.ts +++ b/packages/@aws-cdk/aws-events/lib/index.ts @@ -2,7 +2,6 @@ export * from './rule'; export * from './rule-ref'; export * from './target'; export * from './event-pattern'; -export * from './input-options'; // AWS::Events CloudFormation Resources: export * from './events.generated'; diff --git a/packages/@aws-cdk/aws-events/lib/input-options.ts b/packages/@aws-cdk/aws-events/lib/input-options.ts deleted file mode 100644 index f45cedc13a94e..0000000000000 --- a/packages/@aws-cdk/aws-events/lib/input-options.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Specifies settings that provide custom input to an Amazon CloudWatch Events - * rule target based on certain event data. - * - * @see https://docs.aws.amazon.com/AmazonCloudWatchEvents/latest/APIReference/API_InputTransformer.html - */ -export interface TargetInputTemplate { - /** - * Input template where you can use the values of the keys from - * inputPathsMap to customize the data sent to the target. Enclose each - * InputPathsMaps value in brackets: - * - * The value passed here will be double-quoted to indicate it's a string value. - * This option is mutually exclusive with `jsonTemplate`. - * - * @example - * - * { - * textTemplate: 'Build started', - * pathsMap: { - * buildid: '$.detail.id' - * } - * } - */ - readonly textTemplate?: string; - - /** - * Input template where you can use the values of the keys from - * inputPathsMap to customize the data sent to the target. Enclose each - * InputPathsMaps value in brackets: - * - * This option is mutually exclusive with `textTemplate`. - * - * @example - * - * { - * jsonTemplate: '{ "commands": }' , - * pathsMap: { - * commandsToRun: '$.detail.commands' - * } - * } - * - */ - readonly jsonTemplate?: any; - - /** - * Map of JSON paths to be extracted from the event. These are key-value - * pairs, where each value is a JSON path. You must use JSON dot notation, - * not bracket notation. - */ - readonly pathsMap?: { [key: string]: string }; -} diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 5bd65aaac7203..af2e00f2454d7 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,7 +1,6 @@ import { CfnOutput, Construct, Resource, Token } from '@aws-cdk/cdk'; import { EventPattern } from './event-pattern'; import { CfnRule } from './events.generated'; -import { TargetInputTemplate } from './input-options'; import { EventRuleAttributes, IEventRule } from './rule-ref'; import { IEventRuleTarget } from './target'; import { mergeEventPattern } from './util'; @@ -120,11 +119,10 @@ export class EventRule extends Resource implements IEventRule { * * No-op if target is undefined. */ - public addTarget(target?: IEventRuleTarget, inputOptions?: TargetInputTemplate) { + public addTarget(target?: IEventRuleTarget) { if (!target) { return; } - const self = this; - const targetProps = target.asEventRuleTarget(this.ruleArn, this.node.uniqueId); + const targetProps = target.bind(this); // check if a target with this ID already exists if (this.targets.find(t => t.id === targetProps.id)) { @@ -133,41 +131,11 @@ export class EventRule extends Resource implements IEventRule { this.targets.push({ ...targetProps, - inputTransformer: renderTransformer(), + inputTransformer: targetProps.inputTemplate !== undefined && targetProps.inputPathsMap !== undefined ? { + inputTemplate: targetProps.inputTemplate, + inputPathsMap: targetProps.inputPathsMap, + } : undefined, }); - - function renderTransformer(): CfnRule.InputTransformerProperty | undefined { - if (!inputOptions) { - return undefined; - } - - if (inputOptions.jsonTemplate && inputOptions.textTemplate) { - throw new Error('"jsonTemplate" and "textTemplate" are mutually exclusive'); - } - - if (!inputOptions.jsonTemplate && !inputOptions.textTemplate) { - throw new Error('One of "jsonTemplate" or "textTemplate" are required'); - } - - let inputTemplate: any; - - if (inputOptions.jsonTemplate) { - inputTemplate = typeof inputOptions.jsonTemplate === 'string' - ? inputOptions.jsonTemplate - : self.node.stringifyJson(inputOptions.jsonTemplate); - } else { - inputTemplate = typeof(inputOptions.textTemplate) === 'string' - // Newline separated list of JSON-encoded strings - ? inputOptions.textTemplate.split('\n').map(x => self.node.stringifyJson(x)).join('\n') - // Some object, stringify it, then stringify the string for proper escaping - : self.node.stringifyJson(self.node.stringifyJson(inputOptions.textTemplate)); - } - - return { - inputPathsMap: inputOptions.pathsMap, - inputTemplate - }; - } } /** diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index f885562103f5c..b1498ff33840f 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -1,6 +1,24 @@ import { CfnRule } from './events.generated'; +import { IEventRule } from './rule-ref'; -export interface EventRuleTargetProps { +/** + * An abstract target for EventRules. + */ +export interface IEventRuleTarget { + /** + * Returns the rule target specification. + * NOTE: Do not use the various `inputXxx` options. They can be set in a call to `addTarget`. + * + * @param ruleArn The ARN of the CloudWatch Event Rule that would trigger this target. + * @param ruleUniqueId A unique ID for this rule. Can be used to implement idempotency. + */ + bind(rule: IEventRule): EventRuleTargetProperties; +} + +/** + * Properties for an event rule target + */ +export interface EventRuleTargetProperties { /** * A unique, user-defined identifier for the target. Acceptable values * include alphanumeric characters, periods (.), hyphens (-), and @@ -39,18 +57,24 @@ export interface EventRuleTargetProps { * Command. */ readonly runCommandParameters?: CfnRule.RunCommandParametersProperty; -} -/** - * An abstract target for EventRules. - */ -export interface IEventRuleTarget { /** - * Returns the rule target specification. - * NOTE: Do not use the various `inputXxx` options. They can be set in a call to `addTarget`. - * - * @param ruleArn The ARN of the CloudWatch Event Rule that would trigger this target. - * @param ruleUniqueId A unique ID for this rule. Can be used to implement idempotency. + * Literal input to the target service (must be valid JSON) + */ + readonly input?: string; + + /** + * JsonPath to take input from the input event + */ + readonly inputPath?: string; + + /** + * Input template to insert paths map into + */ + readonly inputTemplate?: string; + + /** + * Paths map to extract values from event and insert into `inputTemplate` */ - asEventRuleTarget(ruleArn: string, ruleUniqueId: string): EventRuleTargetProps; + readonly inputPathsMap?: {[key: string]: string}; } diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index b15c1187d5ef1..b0f5254bdba1b 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -1,8 +1,8 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { IEventRuleTarget } from '../lib'; +import { IEventRule, IEventRuleTarget } from '../lib'; import { EventRule } from '../lib/rule'; // tslint:disable:object-literal-key-quotes @@ -155,7 +155,7 @@ export = { 'targets can be added via props or addTarget with input transformer'(test: Test) { const stack = new cdk.Stack(); const t1: IEventRuleTarget = { - asEventRuleTarget: () => ({ + bind: () => ({ id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' } @@ -163,10 +163,14 @@ export = { }; const t2: IEventRuleTarget = { - asEventRuleTarget: () => ({ + bind: () => ({ id: 'T2', arn: 'ARN2', - roleArn: 'IAM-ROLE-ARN' + roleArn: 'IAM-ROLE-ARN', + inputTemplate: 'This is ', + inputPathsMap: { + bla: '$.detail.bla' + }, }) }; @@ -175,12 +179,7 @@ export = { scheduleExpression: 'rate(5 minutes)' }); - rule.addTarget(t2, { - textTemplate: 'This is ', - pathsMap: { - bla: '$.detail.bla' - } - }); + rule.addTarget(t2); expect(stack).toMatch({ "Resources": { @@ -218,41 +217,43 @@ export = { 'input template can contain tokens'(test: Test) { const stack = new cdk.Stack(); - const t1: IEventRuleTarget = { - asEventRuleTarget: () => ({ - id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' } - }) - }; - - const t2: IEventRuleTarget = { asEventRuleTarget: () => ({ id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN' }) }; - const t3: IEventRuleTarget = { asEventRuleTarget: () => ({ id: 'T3', arn: 'ARN3' }) }; - const t4: IEventRuleTarget = { asEventRuleTarget: () => ({ id: 'T4', arn: 'ARN4' }) }; const rule = new EventRule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); // a plain string should just be stringified (i.e. double quotes added and escaped) - rule.addTarget(t2, { - textTemplate: 'Hello, "world"' + rule.addTarget({ + bind: () => ({ + id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', input: 'Hello, "world"' + }) }); // tokens are used here (FnConcat), but this is a text template so we // expect it to be wrapped in double quotes automatically for us. - rule.addTarget(t1, { - textTemplate: cdk.Fn.join('', [ 'a', 'b' ]).toString() + rule.addTarget({ + bind: () => ({ + id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' }, + inpuTemplate: cdk.Fn.join('', [ 'a', 'b' ]).toString() + }) }); // jsonTemplate can be used to format JSON documents with replacements - rule.addTarget(t3, { - jsonTemplate: '{ "foo": }', - pathsMap: { - bar: '$.detail.bar' - } + rule.addTarget({ + bind: () => ({ + id: 'T3', arn: 'ARN3', + inputTemplate: '{ "foo": }', + inputPathsMap: { + bar: '$.detail.bar' + } + }) }); // tokens can also used for JSON templates, but that means escaping is // the responsibility of the user. - rule.addTarget(t4, { - jsonTemplate: cdk.Fn.join(' ', ['"', 'hello', '\"world\"', '"']), + rule.addTarget({ + bind: () => ({ + id: 'T4', arn: 'ARN4', + inputTemplate: cdk.Fn.join(' ', ['"', 'hello', '\"world\"', '"']), + }) }); expect(stack).toMatch({ @@ -314,9 +315,9 @@ export = { let receivedRuleId = 'FAIL'; const t1: IEventRuleTarget = { - asEventRuleTarget: (ruleArn: string, ruleId: string) => { - receivedRuleArn = ruleArn; - receivedRuleId = ruleId; + bind: (eventRule: IEventRule) => { + receivedRuleArn = eventRule.ruleArn; + receivedRuleId = eventRule.node.uniqueId; return { id: 'T1', @@ -350,114 +351,6 @@ export = { test.done(); }, - 'json template': { - 'can just be a JSON object'(test: Test) { - // GIVEN - const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { - scheduleExpression: 'rate(1 minute)' - }); - - // WHEN - rule.addTarget(new SomeTarget(), { - jsonTemplate: { SomeObject: 'withAValue' }, - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { - Targets: [ - { - InputTransformer: { - InputTemplate: "{\"SomeObject\":\"withAValue\"}" - }, - } - ] - })); - test.done(); - }, - }, - - 'text templates': { - 'strings with newlines are serialized to a newline-delimited list of JSON strings'(test: Test) { - // GIVEN - const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { - scheduleExpression: 'rate(1 minute)' - }); - - // WHEN - rule.addTarget(new SomeTarget(), { - textTemplate: 'I have\nmultiple lines', - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { - Targets: [ - { - InputTransformer: { - InputTemplate: "\"I have\"\n\"multiple lines\"" - }, - } - ] - })); - - test.done(); - }, - - 'escaped newlines are not interpreted as newlines'(test: Test) { - // GIVEN - const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { - scheduleExpression: 'rate(1 minute)' - }); - - // WHEN - rule.addTarget(new SomeTarget(), { - textTemplate: 'this is not\\na real newline', - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { - Targets: [ - { - InputTransformer: { - InputTemplate: "\"this is not\\\\na real newline\"" - }, - } - ] - })); - - test.done(); - }, - - 'can use Tokens in text templates'(test: Test) { - // GIVEN - const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { - scheduleExpression: 'rate(1 minute)' - }); - - const world = new cdk.Token(() => 'world'); - - // WHEN - rule.addTarget(new SomeTarget(), { - textTemplate: `hello ${world}`, - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { - Targets: [ - { - InputTransformer: { - InputTemplate: "\"hello world\"" - }, - } - ] - })); - - test.done(); - } - }, 'rule can be disabled'(test: Test) { // GIVEN @@ -493,7 +386,7 @@ export = { }; class SomeTarget implements IEventRuleTarget { - public asEventRuleTarget() { + public bind() { return { id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' } }; From fffe4ad8157a142ad46902e6cf5e20d86bffa981 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 9 May 2019 11:25:40 +0200 Subject: [PATCH 02/33] Trying to abstract over CWE input --- .../@aws-cdk/aws-events-targets/lib/sns.ts | 18 ++- packages/@aws-cdk/aws-events/lib/index.ts | 1 + packages/@aws-cdk/aws-events/lib/input.ts | 108 ++++++++++++++++++ packages/@aws-cdk/aws-events/lib/rule.ts | 8 +- packages/@aws-cdk/aws-events/lib/target.ts | 14 ++- .../@aws-cdk/aws-events/test/test.rule.ts | 27 ++--- packages/decdk/package-lock.json | 2 +- tools/cdk-build-tools/package-lock.json | 2 +- 8 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 packages/@aws-cdk/aws-events/lib/input.ts diff --git a/packages/@aws-cdk/aws-events-targets/lib/sns.ts b/packages/@aws-cdk/aws-events-targets/lib/sns.ts index cabc8088f2ace..4b8d3fbde74e8 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sns.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sns.ts @@ -2,6 +2,18 @@ import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import sns = require('@aws-cdk/aws-sns'); +/** + * Customize the SNS Topic Event Target + */ +export interface SnsTopicProps { + /** + * The message to send to the topic + * + * @default the entire CloudWatch event + */ + message?: events.EventTargetInput; +} + /** * Use an SNS topic as a target for AWS CloudWatch event rules. * @@ -13,8 +25,7 @@ import sns = require('@aws-cdk/aws-sns'); * */ export class SnsTopic implements events.IEventRuleTarget { - constructor(public readonly topic: sns.ITopic) { - + constructor(public readonly topic: sns.ITopic, private readonly props: SnsTopicProps = {}) { } /** @@ -23,13 +34,14 @@ export class SnsTopic implements events.IEventRuleTarget { * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions */ - public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { + public bind(_rule: events.EventRule): events.EventRuleTargetProperties { // deduplicated automatically this.topic.grantPublish(new iam.ServicePrincipal('events.amazonaws.com')); return { id: this.topic.node.id, arn: this.topic.topicArn, + input: this.props.message && this.props.message.toInputProperties(), }; } } diff --git a/packages/@aws-cdk/aws-events/lib/index.ts b/packages/@aws-cdk/aws-events/lib/index.ts index 35093e32ed265..81e2633f09a6f 100644 --- a/packages/@aws-cdk/aws-events/lib/index.ts +++ b/packages/@aws-cdk/aws-events/lib/index.ts @@ -1,3 +1,4 @@ +export * from './input'; export * from './rule'; export * from './rule-ref'; export * from './target'; diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts new file mode 100644 index 0000000000000..f282481ddc409 --- /dev/null +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -0,0 +1,108 @@ +import { Token } from '@aws-cdk/cdk'; +import { EventRuleTargetInputProperties } from './target'; + +/** + * The input to send to the event target + */ +export class EventTargetInput { + /** + * Pass text to the event target + * + * May contain strings returned by EventField.from() to substitute in parts of the + * matched event. + */ + public static fromText(text: string): EventTargetInput { + return new EventTargetInput({ input: JSON.stringify(text) }); + } + + /** + * Pass a JSON object to the event target + * + * May contain strings returned by EventField.from() to substitute in parts of the + * matched event. + */ + public static fromObject(obj: any): EventTargetInput { + return new EventTargetInput({ input: JSON.stringify(obj) }); + } + + /** + * Take the event target input from a path in the event JSON + */ + public static fromEventPath(path: string): EventTargetInput { + return new EventTargetInput({ inputPath: path }); + } + + private constructor(private readonly props: EventRuleTargetInputProperties) { + } + + /** + * Return the input properties for this input object + */ + public toInputProperties() { + return this.props; + } +} + +/** + * Represents a field in the event pattern + */ +export class EventField extends Token { + /** + * Extract the event ID from the event + */ + public static get eventId(): string { + return this.fromPath('$.id', 'eventId'); + } + + /** + * Extract the detail type from the event + */ + public static get detailType(): string { + return this.fromPath('$.detail-type', 'detailType'); + } + + /** + * Extract the source from the event + */ + public static get source(): string { + return this.fromPath('$.source', 'source'); + } + + /** + * Extract the account from the event + */ + public static get account(): string { + return this.fromPath('$.account', 'account'); + } + + /** + * Extract the time from the event + */ + public static get time(): string { + return this.fromPath('$.time', 'time'); + } + + /** + * Extract the region from the event + */ + public static get region(): string { + return this.fromPath('$.region', 'region'); + } + + /** + * Extract a custom JSON path from the event + */ + public static fromPath(path: string, nameHint?: string): string { + EventField.fieldCounter += 1; + + nameHint = nameHint || `f${EventField.fieldCounter}`; + + return new EventField(`<${nameHint}>`, path).toString(); + } + + private static fieldCounter = 0; + + private constructor(public readonly key: string, public readonly path: string) { + super(key); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index af2e00f2454d7..46950e93a625b 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -131,9 +131,11 @@ export class EventRule extends Resource implements IEventRule { this.targets.push({ ...targetProps, - inputTransformer: targetProps.inputTemplate !== undefined && targetProps.inputPathsMap !== undefined ? { - inputTemplate: targetProps.inputTemplate, - inputPathsMap: targetProps.inputPathsMap, + input: targetProps.input && targetProps.input.input, + inputPath: targetProps.input && targetProps.input.inputPath, + inputTransformer: targetProps.input && targetProps.input.inputTemplate !== undefined ? { + inputTemplate: targetProps.input.inputTemplate, + inputPathsMap: targetProps.input.inputPathsMap, } : undefined, }); } diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index b1498ff33840f..e795e8e4c73b9 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -58,6 +58,18 @@ export interface EventRuleTargetProperties { */ readonly runCommandParameters?: CfnRule.RunCommandParametersProperty; + /** + * What input to send to the event target + * + * @default the entire event + */ + readonly input?: EventRuleTargetInputProperties; +} + +/** + * The input properties for an event target + */ +export interface EventRuleTargetInputProperties { /** * Literal input to the target service (must be valid JSON) */ @@ -77,4 +89,4 @@ export interface EventRuleTargetProperties { * Paths map to extract values from event and insert into `inputTemplate` */ readonly inputPathsMap?: {[key: string]: string}; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index b0f5254bdba1b..2a49dd06072d1 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { IEventRule, IEventRuleTarget } from '../lib'; +import { EventField, EventTargetInput, IEventRule, IEventRuleTarget } from '../lib'; import { EventRule } from '../lib/rule'; // tslint:disable:object-literal-key-quotes @@ -167,10 +167,7 @@ export = { id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', - inputTemplate: 'This is ', - inputPathsMap: { - bla: '$.detail.bla' - }, + input: EventTargetInput.fromText(`This is ${EventField.fromPath('$.detail.bla', 'bla')}`).toInputProperties(), }) }; @@ -223,7 +220,7 @@ export = { // a plain string should just be stringified (i.e. double quotes added and escaped) rule.addTarget({ bind: () => ({ - id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', input: 'Hello, "world"' + id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', input: EventTargetInput.fromText('Hello, "world"').toInputProperties() }) }); @@ -232,7 +229,7 @@ export = { rule.addTarget({ bind: () => ({ id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' }, - inpuTemplate: cdk.Fn.join('', [ 'a', 'b' ]).toString() + input: EventTargetInput.fromText(cdk.Fn.join('', [ 'a', 'b' ]).toString()).toInputProperties(), }) }); @@ -240,10 +237,7 @@ export = { rule.addTarget({ bind: () => ({ id: 'T3', arn: 'ARN3', - inputTemplate: '{ "foo": }', - inputPathsMap: { - bar: '$.detail.bar' - } + input: EventTargetInput.fromObject({ foo: EventField.fromPath('$.detail.bar') }).toInputProperties() }) }); @@ -252,7 +246,7 @@ export = { rule.addTarget({ bind: () => ({ id: 'T4', arn: 'ARN4', - inputTemplate: cdk.Fn.join(' ', ['"', 'hello', '\"world\"', '"']), + input: EventTargetInput.fromText(cdk.Fn.join(' ', ['"', 'hello', '\"world\"', '"']).toString()).toInputProperties(), }) }); @@ -267,17 +261,13 @@ export = { { "Arn": "ARN2", "Id": "T2", - "InputTransformer": { - "InputTemplate": "\"Hello, \\\"world\\\"\"" - }, + "Input": "\"Hello, \\\"world\\\"\"", "RoleArn": "IAM-ROLE-ARN" }, { "Arn": "ARN1", "Id": "T1", - "InputTransformer": { - "InputTemplate": "\"ab\"" - }, + "Input": "\"ab\"", "KinesisParameters": { "PartitionKeyPath": "partitionKeyPath" } @@ -351,7 +341,6 @@ export = { test.done(); }, - 'rule can be disabled'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/decdk/package-lock.json b/packages/decdk/package-lock.json index d10b94a3f3859..ac19069fa77a7 100644 --- a/packages/decdk/package-lock.json +++ b/packages/decdk/package-lock.json @@ -1,6 +1,6 @@ { "name": "decdk", - "version": "0.30.0", + "version": "0.31.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/cdk-build-tools/package-lock.json b/tools/cdk-build-tools/package-lock.json index 1561e688b35d6..31c4331be5608 100644 --- a/tools/cdk-build-tools/package-lock.json +++ b/tools/cdk-build-tools/package-lock.json @@ -1,6 +1,6 @@ { "name": "cdk-build-tools", - "version": "0.30.0", + "version": "0.31.0", "lockfileVersion": 1, "requires": true, "dependencies": { From ddc7584c1e688cd5870c14856bd69cf01be04757 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 9 May 2019 13:34:05 +0200 Subject: [PATCH 03/33] Make resolve framework to more general, get rid of global options --- packages/@aws-cdk/aws-events/lib/target.ts | 3 +- packages/@aws-cdk/cdk/lib/cfn-concat.ts | 9 +- packages/@aws-cdk/cdk/lib/cfn-condition.ts | 6 +- packages/@aws-cdk/cdk/lib/cfn-reference.ts | 4 +- .../@aws-cdk/cdk/lib/cloudformation-json.ts | 13 +- packages/@aws-cdk/cdk/lib/construct.ts | 10 +- packages/@aws-cdk/cdk/lib/encoding.ts | 151 +++++++++----- packages/@aws-cdk/cdk/lib/fn.ts | 9 +- packages/@aws-cdk/cdk/lib/options.ts | 56 ----- packages/@aws-cdk/cdk/lib/resolve.ts | 195 ++++++++++++------ packages/@aws-cdk/cdk/lib/token-map.ts | 20 +- packages/@aws-cdk/cdk/lib/token.ts | 17 +- packages/@aws-cdk/cdk/lib/util.ts | 4 +- .../cdk/test/test.cloudformation-json.ts | 24 +-- 14 files changed, 299 insertions(+), 222 deletions(-) delete mode 100644 packages/@aws-cdk/cdk/lib/options.ts diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index b1498ff33840f..2ffe01e87e4d7 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -9,8 +9,7 @@ export interface IEventRuleTarget { * Returns the rule target specification. * NOTE: Do not use the various `inputXxx` options. They can be set in a call to `addTarget`. * - * @param ruleArn The ARN of the CloudWatch Event Rule that would trigger this target. - * @param ruleUniqueId A unique ID for this rule. Can be used to implement idempotency. + * @param rule The CloudWatch Event Rule that would trigger this target. */ bind(rule: IEventRule): EventRuleTargetProperties; } diff --git a/packages/@aws-cdk/cdk/lib/cfn-concat.ts b/packages/@aws-cdk/cdk/lib/cfn-concat.ts index fd16ed1046710..4b2d696acf098 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-concat.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-concat.ts @@ -19,4 +19,11 @@ export function cloudFormationConcat(left: any | undefined, right: any | undefin return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; } -import { minimalCloudFormationJoin } from "./instrinsics"; +export class CloudFormationConcat implements IFragmentConcatenator { + public join(left: any, right: any) { + return cloudFormationConcat(left, right); + } +} + +import { IFragmentConcatenator } from "./encoding"; +import { minimalCloudFormationJoin } from "./instrinsics"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cfn-condition.ts b/packages/@aws-cdk/cdk/lib/cfn-condition.ts index 275db39e1207b..94714877b53b1 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-condition.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-condition.ts @@ -1,6 +1,6 @@ import { CfnRefElement } from './cfn-element'; import { Construct } from './construct'; -import { ResolveContext } from './token'; +import { IResolveContext } from './token'; export interface CfnConditionProps { readonly expression?: ICfnConditionExpression; @@ -43,7 +43,7 @@ export class CfnCondition extends CfnRefElement implements ICfnConditionExpressi /** * Synthesizes the condition. */ - public resolve(_context: ResolveContext): any { + public resolve(_context: IResolveContext): any { return { Condition: this.logicalId }; } } @@ -76,7 +76,7 @@ export interface ICfnConditionExpression { /** * Returns a JSON node that represents this condition expression */ - resolve(context: ResolveContext): any; + resolve(context: IResolveContext): any; /** * Returns a string token representation of this condition expression, which diff --git a/packages/@aws-cdk/cdk/lib/cfn-reference.ts b/packages/@aws-cdk/cdk/lib/cfn-reference.ts index 24cb71b37aba9..0cb430a382058 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-reference.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-reference.ts @@ -49,7 +49,7 @@ export class CfnReference extends Reference { Object.defineProperty(this, CFN_REFERENCE_SYMBOL, { value: true }); } - public resolve(context: ResolveContext): any { + public resolve(context: IResolveContext): any { // If we have a special token for this consuming stack, resolve that. Otherwise resolve as if // we are in the same stack. const token = this.replacementTokens.get(context.scope.node.stack); @@ -112,4 +112,4 @@ export class CfnReference extends Reference { import { CfnOutput } from "./cfn-output"; import { Construct, IConstruct } from "./construct"; import { Stack } from "./stack"; -import { ResolveContext, Token } from "./token"; +import { IResolveContext, Token } from "./token"; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation-json.ts index 4f0d79a5d2924..9bb0d2296ab8d 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation-json.ts @@ -1,7 +1,5 @@ -import { IConstruct } from "./construct"; import { isIntrinsic } from "./instrinsics"; -import { resolve } from "./resolve"; -import { Token } from "./token"; +import { IResolveContext, Token } from "./token"; /** * Class for JSON routines that are framework-aware @@ -20,8 +18,8 @@ export class CloudFormationJSON { * @param obj The object to stringify * @param context The Construct from which to resolve any Tokens found in the object */ - public static stringify(obj: any, context: IConstruct): string { - return new Token(() => { + public static stringify(obj: any): string { + return new Token((ctx: IResolveContext) => { // Resolve inner value first so that if they evaluate to literals, we // maintain the type (and discard 'undefined's). // @@ -31,10 +29,7 @@ export class CloudFormationJSON { // deep-escapes any strings inside the intrinsic, so that if literal // strings are used in {Fn::Join} or something, they will end up // escaped in the final JSON output. - const resolved = resolve(obj, { - scope: context, - prefix: [] - }); + const resolved = ctx.resolve(obj); // We can just directly return this value, since resolve() will be called // on our return value anyway. diff --git a/packages/@aws-cdk/cdk/lib/construct.ts b/packages/@aws-cdk/cdk/lib/construct.ts index b553d8c337e9f..6b45b536ec9fa 100644 --- a/packages/@aws-cdk/cdk/lib/construct.ts +++ b/packages/@aws-cdk/cdk/lib/construct.ts @@ -2,7 +2,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { IAspect } from './aspect'; import { CloudFormationJSON } from './cloudformation-json'; import { IDependable } from './dependency'; -import { resolve } from './resolve'; +import { DefaultTokenResolver, resolve } from './resolve'; import { Token } from './token'; import { makeUniqueId } from './uniqueid'; @@ -456,7 +456,8 @@ export class ConstructNode { public resolve(obj: any): any { return resolve(obj, { scope: this.host, - prefix: [] + prefix: [], + resolver: new DefaultTokenResolver(new CloudFormationConcat()), }); } @@ -464,7 +465,7 @@ export class ConstructNode { * Convert an object, potentially containing tokens, to a JSON string */ public stringifyJson(obj: any): string { - return CloudFormationJSON.stringify(obj, this.host).toString(); + return CloudFormationJSON.stringify(obj).toString(); } /** @@ -725,4 +726,5 @@ export interface OutgoingReference { } // Import this _after_ everything else to help node work the classes out in the correct order... -import { Reference } from './reference'; +import { CloudFormationConcat } from './cfn-concat'; +import { Reference } from './reference'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/encoding.ts b/packages/@aws-cdk/cdk/lib/encoding.ts index f556adb75e230..b7eb2e6ee2076 100644 --- a/packages/@aws-cdk/cdk/lib/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/encoding.ts @@ -12,23 +12,8 @@ const QUOTED_BEGIN_STRING_TOKEN_MARKER = regexQuote(BEGIN_STRING_TOKEN_MARKER); const QUOTED_BEGIN_LIST_TOKEN_MARKER = regexQuote(BEGIN_LIST_TOKEN_MARKER); const QUOTED_END_TOKEN_MARKER = regexQuote(END_TOKEN_MARKER); -/** - * Interface that Token joiners implement - */ -export interface ITokenJoiner { - /** - * The name of the joiner. - * - * Must be unique per joiner: this value will be used to assert that there - * is exactly only type of joiner in a join operation. - */ - id: string; - - /** - * Return the language intrinsic that will combine the strings in the given engine - */ - join(fragments: any[]): any; -} +const STRING_TOKEN_REGEX = new RegExp(`${QUOTED_BEGIN_STRING_TOKEN_MARKER}([${VALID_KEY_CHARS}]+)${QUOTED_END_TOKEN_MARKER}`, 'g'); +const LIST_TOKEN_REGEX = new RegExp(`${QUOTED_BEGIN_LIST_TOKEN_MARKER}([${VALID_KEY_CHARS}]+)${QUOTED_END_TOKEN_MARKER}`, 'g'); /** * A string with markers in it that can be resolved to external values @@ -38,44 +23,37 @@ export class TokenString { * Returns a `TokenString` for this string. */ public static forStringToken(s: string) { - return new TokenString(s, QUOTED_BEGIN_STRING_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, QUOTED_END_TOKEN_MARKER); + return new TokenString(s, STRING_TOKEN_REGEX); } /** * Returns a `TokenString` for this string (must be the first string element of the list) */ public static forListToken(s: string) { - return new TokenString(s, QUOTED_BEGIN_LIST_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, QUOTED_END_TOKEN_MARKER); + return new TokenString(s, LIST_TOKEN_REGEX); } - private pattern: string; - - constructor( - private readonly str: string, - quotedBeginMarker: string, - idPattern: string, - quotedEndMarker: string) { - this.pattern = `${quotedBeginMarker}(${idPattern})${quotedEndMarker}`; + constructor(private readonly str: string, private readonly re: RegExp) { } /** * Split string on markers, substituting markers with Tokens */ public split(lookup: (id: string) => Token): TokenizedStringFragments { - const re = new RegExp(this.pattern, 'g'); const ret = new TokenizedStringFragments(); let rest = 0; - let m = re.exec(this.str); + this.re.lastIndex = 0; // Reset + let m = this.re.exec(this.str); while (m) { if (m.index > rest) { ret.addLiteral(this.str.substring(rest, m.index)); } - ret.addUnresolved(lookup(m[1])); + ret.addToken(lookup(m[1])); - rest = re.lastIndex; - m = re.exec(this.str); + rest = this.re.lastIndex; + m = this.re.exec(this.str); } if (rest < this.str.length) { @@ -89,8 +67,8 @@ export class TokenString { * Indicates if this string includes tokens. */ public test(): boolean { - const re = new RegExp(this.pattern, 'g'); - return re.test(this.str); + this.re.lastIndex = 0; // Reset + return this.re.test(this.str); } } @@ -100,32 +78,41 @@ export class TokenString { * Either a literal part of the string, or an unresolved Token. */ type LiteralFragment = { type: 'literal'; lit: any; }; -type UnresolvedFragment = { type: 'unresolved'; token: any; }; -type Fragment = LiteralFragment | UnresolvedFragment; +type TokenFragment = { type: 'token'; token: Token; }; +type IntrinsicFragment = { type: 'intrinsic'; value: any; }; +type Fragment = LiteralFragment | TokenFragment | IntrinsicFragment; /** * Fragments of a string with markers */ class TokenizedStringFragments { - private readonly fragments = new Array(); + public readonly fragments = new Array(); - public get length() { - return this.fragments.length; + public get firstFragment(): Fragment { + return this.fragments[0]; + } + + public get firstValue(): any { + return fragmentValue(this.fragments[0]); } - public get values(): any[] { - return this.fragments.map(f => f.type === 'unresolved' ? f.token : f.lit); + public get length() { + return this.fragments.length; } public addLiteral(lit: any) { this.fragments.push({ type: 'literal', lit }); } - public addUnresolved(token: Token) { - this.fragments.push({ type: 'unresolved', token }); + public addToken(token: Token) { + this.fragments.push({ type: 'token', token }); } - public mapUnresolved(fn: (t: any) => any): TokenizedStringFragments { + public addIntrinsic(value: any) { + this.fragments.push({ type: 'intrinsic', value }); + } + + public mapTokens(fn: (t: any) => any): TokenizedStringFragments { const ret = new TokenizedStringFragments(); for (const f of this.fragments) { @@ -133,15 +120,17 @@ class TokenizedStringFragments { case 'literal': ret.addLiteral(f.lit); break; - case 'unresolved': - const mappedToken = fn(f.token); - - if (unresolved(mappedToken)) { - ret.addUnresolved(mappedToken); + case 'token': + const mapped = fn(f.token); + if (isTokenObject(mapped)) { + ret.addToken(mapped); } else { - ret.addLiteral(mappedToken); + ret.addIntrinsic(mapped); } break; + case 'intrinsic': + ret.addIntrinsic(f.value); + break; } } @@ -149,18 +138,18 @@ class TokenizedStringFragments { } /** - * Combine the resolved string fragments using the Tokens to join. + * Combine the string fragments using the given joiner. * - * Resolves the result. + * If there are any */ - public join(concat: ConcatFunc): any { - if (this.fragments.length === 0) { return concat(undefined, undefined); } + public join(concat: IFragmentConcatenator): any { + if (this.fragments.length === 0) { return concat.join(undefined, undefined); } const values = this.fragments.map(fragmentValue); while (values.length > 1) { const prefix = values.splice(0, 2); - values.splice(0, 0, concat(prefix[0], prefix[1])); + values.splice(0, 0, concat.join(prefix[0], prefix[1])); } return values[0]; @@ -169,9 +158,15 @@ class TokenizedStringFragments { /** * Resolve the value from a single fragment + * + * If the fragment is a Token, return the string encoding of the Token. */ function fragmentValue(fragment: Fragment): any { - return fragment.type === 'literal' ? fragment.lit : fragment.token; + switch (fragment.type) { + case 'literal': return fragment.lit; + case 'token': return fragment.token.toString(); + case 'intrinsic': return fragment.value; + } } /** @@ -183,8 +178,40 @@ function regexQuote(s: string) { /** * Function used to concatenate symbols in the target document language + * + * Interface so it could potentially be exposed over jsii. */ -export type ConcatFunc = (left: any | undefined, right: any | undefined) => any; +export interface IFragmentConcatenator { + /** + * Join the fragment on the left and on the right + */ + join(left: any | undefined, right: any | undefined): any; +} + +/** + * Concatenator that disregards the input + * + * Can be used when traversing the tokens is important, but the + * result isn't. + */ +export class NullConcat implements IFragmentConcatenator { + public join(_left: any | undefined, _right: any | undefined): any { + return undefined; + } +} + +/** + * Converts all fragments to strings and concats those + * + * Drops 'undefined's. + */ +export class StringConat implements IFragmentConcatenator { + public join(left: any | undefined, right: any | undefined): any { + if (left === undefined) { return right !== undefined ? `${right}` : undefined; } + if (right === undefined) { return `${left}`; } + return `${left}${right}`; + } +} export function containsListTokenElement(xs: any[]) { return xs.some(x => typeof(x) === 'string' && TokenString.forListToken(x).test()); @@ -205,4 +232,14 @@ export function unresolved(obj: any): boolean { } else { return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; } +} + +/** + * Whether x is literally a Token object + * + * Can't use Token.isToken() because that has been co-opted + * to mean something else. + */ +function isTokenObject(x: any): x is Token { + return typeof(x) === 'object' && x !== null && Token.isToken(x); } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/fn.ts b/packages/@aws-cdk/cdk/lib/fn.ts index 4bd33656fe3ac..f15c31f0255b9 100644 --- a/packages/@aws-cdk/cdk/lib/fn.ts +++ b/packages/@aws-cdk/cdk/lib/fn.ts @@ -1,7 +1,6 @@ import { ICfnConditionExpression } from './cfn-condition'; import { minimalCloudFormationJoin } from './instrinsics'; -import { resolve } from './resolve'; -import { ResolveContext, Token } from './token'; +import { IResolveContext, Token } from './token'; // tslint:disable:max-line-length @@ -650,7 +649,7 @@ class FnJoin extends Token { this.listOfValues = listOfValues; } - public resolve(context: ResolveContext): any { + public resolve(context: IResolveContext): any { if (Token.isToken(this.listOfValues)) { // This is a list token, don't try to do smart things with it. return { 'Fn::Join': [ this.delimiter, this.listOfValues ] }; @@ -667,10 +666,10 @@ class FnJoin extends Token { * if two concatenated elements are literal strings (not tokens), then pre-concatenate them with the delimiter, to * generate shorter output. */ - private resolveValues(context: ResolveContext) { + private resolveValues(context: IResolveContext) { if (this._resolvedValues) { return this._resolvedValues; } - const resolvedValues = this.listOfValues.map(e => resolve(e, context)); + const resolvedValues = this.listOfValues.map(context.resolve); return this._resolvedValues = minimalCloudFormationJoin(this.delimiter, resolvedValues); } } diff --git a/packages/@aws-cdk/cdk/lib/options.ts b/packages/@aws-cdk/cdk/lib/options.ts deleted file mode 100644 index 8fb5bc90eee16..0000000000000 --- a/packages/@aws-cdk/cdk/lib/options.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Token } from "./token"; - -/** - * Function used to preprocess Tokens before resolving - */ -export type CollectFunc = (token: Token) => void; - -/** - * Global options for resolve() - * - * Because there are many independent calls to resolve(), some losing context, - * we cannot simply pass through options at each individual call. Instead, - * we configure global context at the stack synthesis level. - */ -export class ResolveConfiguration { - private readonly options = new Array(); - - public push(options: ResolveOptions): IOptionsContext { - this.options.push(options); - - return { - pop: () => { - if (this.options.length === 0 || this.options[this.options.length - 1] !== options) { - throw new Error('ResolveConfiguration push/pop mismatch'); - } - this.options.pop(); - } - }; - } - - public get collect(): CollectFunc | undefined { - for (let i = this.options.length - 1; i >= 0; i--) { - const ret = this.options[i].collect; - if (ret !== undefined) { return ret; } - } - return undefined; - } -} - -interface IOptionsContext { - pop(): void; -} - -interface ResolveOptions { - /** - * What function to use to preprocess Tokens before resolving them - */ - collect?: CollectFunc; -} - -const glob = global as any; - -/** - * Singleton instance of resolver options - */ -export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index beb21cb17b6b4..4ec2af1f5d992 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -1,13 +1,26 @@ import { IConstruct } from './construct'; -import { containsListTokenElement, TokenString, unresolved } from "./encoding"; -import { RESOLVE_OPTIONS } from "./options"; -import { isResolvedValuePostProcessor, RESOLVE_METHOD, ResolveContext, Token } from "./token"; +import { containsListTokenElement, IFragmentConcatenator, NullConcat, TokenString, unresolved } from "./encoding"; +import { IResolveContext, isResolvedValuePostProcessor, RESOLVE_METHOD, Token } from "./token"; import { TokenMap } from './token-map'; // This file should not be exported to consumers, resolving should happen through Construct.resolve() const tokenMap = TokenMap.instance(); +/** + * Options to the resolve() operation + * + * NOT the same as the ResolveContext; ResolveContext is exposed to Token + * implementors and resolution hooks, whereas this struct is just to bundle + * a number of things that would otherwise be arguments to resolve() in a + * readable way. + */ +export interface IResolveOptions { + scope: IConstruct; + resolver: ITokenResolver; + prefix: string[]; +} + /** * Resolves an object by evaluating all tokens and removing any undefined or empty objects or arrays. * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. @@ -15,11 +28,22 @@ const tokenMap = TokenMap.instance(); * @param obj The object to resolve. * @param prefix Prefix key path components for diagnostics. */ -export function resolve(obj: any, context: ResolveContext): any { - const pathName = '/' + context.prefix.join('/'); +export function resolve(obj: any, options: IResolveOptions): any { + const pathName = '/' + options.prefix.join('/'); + + /** + * Make a new resolution context + */ + function makeContext(appendPath?: string): IResolveContext { + const newPrefix = appendPath !== undefined ? options.prefix.concat([appendPath]) : options.prefix; + return { + scope: options.scope, + resolve(x: any) { return resolve(x, { ...options, prefix: newPrefix }); } + }; + } // protect against cyclic references by limiting depth. - if (context.prefix.length > 200) { + if (options.prefix.length > 200) { throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); } @@ -51,7 +75,11 @@ export function resolve(obj: any, context: ResolveContext): any { // string - potentially replace all stringified Tokens // if (typeof(obj) === 'string') { - return resolveStringTokens(obj, context); + const str = TokenString.forStringToken(obj); + if (str.test()) { + return options.resolver.resolveString(str, makeContext()); + } + return obj; } // @@ -68,11 +96,11 @@ export function resolve(obj: any, context: ResolveContext): any { if (Array.isArray(obj)) { if (containsListTokenElement(obj)) { - return resolveListTokens(obj, context); + return options.resolver.resolveList(obj, makeContext()); } const arr = obj - .map((x, i) => resolve(x, { ...context, prefix: context.prefix.concat(i.toString()) })) + .map((x, i) => makeContext(`${i}`).resolve(x)) .filter(x => typeof(x) !== 'undefined'); return arr; @@ -83,18 +111,7 @@ export function resolve(obj: any, context: ResolveContext): any { // if (unresolved(obj)) { - const collect = RESOLVE_OPTIONS.collect; - if (collect) { collect(obj); } - - const resolved = obj[RESOLVE_METHOD](context); - - let deepResolved = resolve(resolved, context); - - if (isResolvedValuePostProcessor(obj)) { - deepResolved = obj.postProcess(deepResolved, context); - } - - return deepResolved; + return options.resolver.resolveToken(obj, makeContext()); } // @@ -110,12 +127,12 @@ export function resolve(obj: any, context: ResolveContext): any { const result: any = { }; for (const key of Object.keys(obj)) { - const resolvedKey = resolve(key, context); + const resolvedKey = makeContext().resolve(key); if (typeof(resolvedKey) !== 'string') { throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); } - const value = resolve(obj[key], {...context, prefix: context.prefix.concat(key) }); + const value = makeContext(key).resolve(obj[key]); // skip undefined if (typeof(value) === 'undefined') { @@ -128,23 +145,109 @@ export function resolve(obj: any, context: ResolveContext): any { return result; } +/** + * How to resolve tokens + */ +export interface ITokenResolver { + /** + * Resolve a single token + */ + resolveToken(t: Token, context: IResolveContext): any; + + /** + * Resolve a string with at least one stringified token in it + * + * (May use concatenation) + */ + resolveString(s: TokenString, context: IResolveContext): any; + + /** + * Resolve a tokenized list + */ + resolveList(l: string[], context: IResolveContext): any; +} + +/** + * Default resolver implementation + */ +export class DefaultTokenResolver implements ITokenResolver { + constructor(private readonly concat: IFragmentConcatenator) { + } + + /** + * Default Token resolution + * + * Resolve the Token, recurse into whatever it returns, + * then finally post-process it. + */ + public resolveToken(t: Token, context: IResolveContext) { + let resolved = t[RESOLVE_METHOD](context); + + // The token might have returned more values that need resolving, recurse + resolved = context.resolve(resolved); + + if (isResolvedValuePostProcessor(t)) { + resolved = t.postProcess(resolved, context); + } + + return resolved; + } + + /** + * Resolve string fragments to Tokens + */ + public resolveString(s: TokenString, context: IResolveContext) { + const fragments = s.split(tokenMap.lookupToken.bind(tokenMap)); + + // require() here to break cyclic dependencies + const ret = fragments.mapTokens(context.resolve).join(this.concat); + + // Recurse + return context.resolve(ret); + } + + public resolveList(xs: string[], context: IResolveContext) { + // Must be a singleton list token, because concatenation is not allowed. + if (xs.length !== 1) { + throw new Error(`Cannot add elements to list token, got: ${xs}`); + } + + const str = TokenString.forListToken(xs[0]); + const fragments = str.split(tokenMap.lookupToken.bind(tokenMap)); + if (fragments.length !== 1) { + throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); + } + + return fragments.mapTokens(context.resolve).firstValue; + } + +} + /** * Find all Tokens that are used in the given structure */ export function findTokens(scope: IConstruct, fn: () => any): Token[] { - const ret = new Array(); + const resolver = new RememberingTokenResolver(new NullConcat()); - const options = RESOLVE_OPTIONS.push({ collect: ret.push.bind(ret) }); try { - resolve(fn(), { - scope, - prefix: [] - }); + resolve(fn(), { scope, prefix: [], resolver }); } finally { - options.pop(); + // Swallow potential errors that might occur because we might not have validate()d. } - return ret; + return Array.from(resolver.tokensSeen); +} + +/** + * Remember all Tokens encountered while resolving + */ +export class RememberingTokenResolver extends DefaultTokenResolver { + public readonly tokensSeen = new Set(); + + public resolveToken(t: Token, context: IResolveContext) { + this.tokensSeen.add(t); + return super.resolveToken(t, context); + } } /** @@ -155,32 +258,4 @@ export function findTokens(scope: IConstruct, fn: () => any): Token[] { */ function isConstruct(x: any): boolean { return x._children !== undefined && x._metadata !== undefined; -} - -/** - * Replace any Token markers in this string with their resolved values - */ -function resolveStringTokens(s: string, context: ResolveContext): any { - const str = TokenString.forStringToken(s); - const fragments = str.split(tokenMap.lookupToken.bind(tokenMap)); - // require() here to break cyclic dependencies - const ret = fragments.mapUnresolved(x => resolve(x, context)).join(require('./cfn-concat').cloudFormationConcat); - if (unresolved(ret)) { - return resolve(ret, context); - } - return ret; -} - -function resolveListTokens(xs: string[], context: ResolveContext): any { - // Must be a singleton list token, because concatenation is not allowed. - if (xs.length !== 1) { - throw new Error(`Cannot add elements to list token, got: ${xs}`); - } - - const str = TokenString.forListToken(xs[0]); - const fragments = str.split(tokenMap.lookupToken.bind(tokenMap)); - if (fragments.length !== 1) { - throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); - } - return fragments.mapUnresolved(x => resolve(x, context)).values[0]; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/token-map.ts b/packages/@aws-cdk/cdk/lib/token-map.ts index 3dd31ad0d8481..64ca8add16c66 100644 --- a/packages/@aws-cdk/cdk/lib/token-map.ts +++ b/packages/@aws-cdk/cdk/lib/token-map.ts @@ -49,6 +49,18 @@ export class TokenMap { return [`${BEGIN_LIST_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`]; } + /** + * Lookup a token from an encoded value + */ + public tokenFromEncoding(x: any): Token | undefined { + if (typeof 'x' === 'string') { return this.lookupString(x); } + if (Array.isArray(x)) { return this.lookupList(x); } + if (typeof x === 'object' && x !== null && Token.isToken(x)) { + return x as Token; + } + return undefined; + } + /** * Reverse a string representation into a Token object */ @@ -56,8 +68,8 @@ export class TokenMap { const str = TokenString.forStringToken(s); const fragments = str.split(this.lookupToken.bind(this)); if (fragments.length === 1) { - const v = fragments.values[0]; - if (typeof v !== 'string') { return v as Token; } + const first = fragments.firstFragment; + if (first.type === 'token') { return first.token; } } return undefined; } @@ -70,8 +82,8 @@ export class TokenMap { const str = TokenString.forListToken(xs[0]); const fragments = str.split(this.lookupToken.bind(this)); if (fragments.length === 1) { - const v = fragments.values[0]; - if (typeof v !== 'string') { return v as Token; } + const first = fragments.firstFragment; + if (first.type === 'token') { return first.token; } } return undefined; } diff --git a/packages/@aws-cdk/cdk/lib/token.ts b/packages/@aws-cdk/cdk/lib/token.ts index d78bb418d3ab7..86974f303b241 100644 --- a/packages/@aws-cdk/cdk/lib/token.ts +++ b/packages/@aws-cdk/cdk/lib/token.ts @@ -64,10 +64,10 @@ export class Token { /** * @returns The resolved value for this token. */ - public resolve(_context: ResolveContext): any { + public resolve(context: IResolveContext): any { let value = this.valueOrFunction; if (typeof(value) === 'function') { - value = value(); + value = value(context); } return value; @@ -137,9 +137,16 @@ export class Token { /** * Current resolution context for tokens */ -export interface ResolveContext { +export interface IResolveContext { + /** + * The scope from which resolution has been initiated + */ readonly scope: IConstruct; - readonly prefix: string[]; + + /** + * Resolve an inner object + */ + resolve(x: any): any; } /** @@ -149,7 +156,7 @@ export interface IResolvedValuePostProcessor { /** * Process the completely resolved value, after full recursion/resolution has happened */ - postProcess(input: any, context: ResolveContext): any; + postProcess(input: any, context: IResolveContext): any; } /** diff --git a/packages/@aws-cdk/cdk/lib/util.ts b/packages/@aws-cdk/cdk/lib/util.ts index ba83bfc00f81b..477f61e9015bd 100644 --- a/packages/@aws-cdk/cdk/lib/util.ts +++ b/packages/@aws-cdk/cdk/lib/util.ts @@ -1,5 +1,5 @@ import { IConstruct } from "./construct"; -import { IResolvedValuePostProcessor, ResolveContext, Token } from "./token"; +import { IResolveContext, IResolvedValuePostProcessor, Token } from "./token"; /** * Given an object, converts all keys to PascalCase given they are currently in camel case. @@ -80,7 +80,7 @@ export class PostResolveToken extends Token implements IResolvedValuePostProcess super(value); } - public postProcess(o: any, _context: ResolveContext): any { + public postProcess(o: any, _context: IResolveContext): any { return this.processor(o); } } diff --git a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts index 1686cd6c5d155..02b5242bdfc6d 100644 --- a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts @@ -23,7 +23,7 @@ export = { const fido = { name: 'Fido', speaks: token }; // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido, stack)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"woof woof"}'); @@ -40,7 +40,7 @@ export = { const fido = { name: 'Fido', speaks: `deep ${token}` }; // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido, stack)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"deep woof woof"}'); @@ -57,8 +57,8 @@ export = { // WHEN test.equal(evaluateCFN(stack.node.resolve(embedded)), "the number is 1"); - test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ embedded }, stack))), "{\"embedded\":\"the number is 1\"}"); - test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ num }, stack))), "{\"num\":1}"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ embedded }))), "{\"embedded\":\"the number is 1\"}"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ num }))), "{\"num\":1}"); test.done(); }, @@ -68,7 +68,7 @@ export = { const stack = new Stack(); for (const token of tokensThatResolveTo('pong!')) { // WHEN - const stringified = CloudFormationJSON.stringify(`ping? ${token}`, stack); + const stringified = CloudFormationJSON.stringify(`ping? ${token}`); // THEN test.equal(evaluateCFN(stack.node.resolve(stringified)), '"ping? pong!"'); @@ -83,7 +83,7 @@ export = { const bucketName = new Token({ Ref: 'MyBucket' }); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: bucketName }, stack)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: bucketName })); // THEN const context = {MyBucket: 'TheName'}; @@ -108,7 +108,7 @@ export = { }, })); - const stringified = CloudFormationJSON.stringify(fakeIntrinsics, stack); + const stringified = CloudFormationJSON.stringify(fakeIntrinsics); test.equal(evaluateCFN(stack.node.resolve(stringified)), '{"a":{"Fn::GetArtifactAtt":{"key":"val"}},"b":{"Fn::GetParam":["val1","val2"]}}'); @@ -124,7 +124,7 @@ export = { const resolved = stack.node.resolve(CloudFormationJSON.stringify({ literal: 'I can also "contain" quotes', token - }, stack)); + })); // THEN const expected = '{"literal":"I can also \\"contain\\" quotes","token":"HelloThis\\nIsVery \\"cool\\""}'; @@ -140,7 +140,7 @@ export = { const combinedName = Fn.join('', [ 'The bucket name is ', bucketName.toString() ]); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: combinedName }, stack)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: combinedName })); // THEN const context = {MyBucket: 'TheName'}; @@ -157,7 +157,7 @@ export = { // WHEN const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - }, stack)); + })); // THEN test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: woof"}'); @@ -173,7 +173,7 @@ export = { // WHEN const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - }, stack)); + })); // THEN const context = {Something: 'woof woof'}; @@ -190,7 +190,7 @@ export = { // WHEN const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - }, stack)); + })); // THEN test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: \\"woof\\""}'); From a397a2e4d974e456d5404703d768a34d7057508e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 9 May 2019 16:19:13 +0200 Subject: [PATCH 04/33] asEventRuleTarget => bind --- packages/@aws-cdk/aws-events-targets/lib/lambda.ts | 6 +++--- packages/@aws-cdk/aws-events-targets/lib/sns.ts | 2 +- packages/@aws-cdk/aws-iam/lib/policy-document.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts index 4384dd82a12a8..873cd84981388 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts @@ -18,13 +18,13 @@ export class LambdaFunction implements events.IEventRuleTarget { * Returns a RuleTarget that can be used to trigger this Lambda as a * result from a CloudWatch event. */ - public asEventRuleTarget(ruleArn: string, ruleId: string): events.EventRuleTargetProps { - const permissionId = `AllowEventRule${ruleId}`; + public bind(rule: events.IEventRule): events.EventRuleTargetProperties { + const permissionId = `AllowEventRule${rule.node.uniqueId}`; if (!this.handler.node.tryFindChild(permissionId)) { this.handler.addPermission(permissionId, { action: 'lambda:InvokeFunction', principal: new iam.ServicePrincipal('events.amazonaws.com'), - sourceArn: ruleArn + sourceArn: rule.ruleArn }); } diff --git a/packages/@aws-cdk/aws-events-targets/lib/sns.ts b/packages/@aws-cdk/aws-events-targets/lib/sns.ts index cabc8088f2ace..edddcb611f658 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sns.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sns.ts @@ -23,7 +23,7 @@ export class SnsTopic implements events.IEventRuleTarget { * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions */ - public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { + public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { // deduplicated automatically this.topic.grantPublish(new iam.ServicePrincipal('events.amazonaws.com')); diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index 7ebf7565dc6a6..f4ddba272ecd6 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -23,7 +23,7 @@ export class PolicyDocument extends cdk.Token implements cdk.IResolvedValuePostP this._autoAssignSids = true; } - public resolve(_context: cdk.ResolveContext): any { + public resolve(_context: cdk.IResolveContext): any { if (this.isEmpty) { return undefined; } @@ -40,7 +40,7 @@ export class PolicyDocument extends cdk.Token implements cdk.IResolvedValuePostP /** * Removes duplicate statements */ - public postProcess(input: any, _context: cdk.ResolveContext): any { + public postProcess(input: any, _context: cdk.IResolveContext): any { if (!input || !input.Statement) { return input; } @@ -513,7 +513,7 @@ export class PolicyStatement extends cdk.Token { // // Serialization // - public resolve(_context: cdk.ResolveContext): any { + public resolve(_context: cdk.IResolveContext): any { return this.toJson(); } @@ -589,7 +589,7 @@ class StackDependentToken extends cdk.Token { super(); } - public resolve(context: cdk.ResolveContext) { + public resolve(context: cdk.IResolveContext) { return this.fn(context.scope.node.stack); } } @@ -600,7 +600,7 @@ class ServicePrincipalToken extends cdk.Token { super(); } - public resolve(ctx: cdk.ResolveContext) { + public resolve(ctx: cdk.IResolveContext) { const region = this.opts.region || ctx.scope.node.stack.region; const fact = RegionInfo.get(region).servicePrincipal(this.service); return fact || Default.servicePrincipal(this.service, region, ctx.scope.node.stack.urlSuffix); From 9c0f3bfa20e810f5d48def9d8e8a9788c299ff52 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 May 2019 10:11:05 +0200 Subject: [PATCH 05/33] Group CloudFormation aware routines together --- packages/@aws-cdk/cdk/lib/cfn-concat.ts | 29 -------------- ...rmation-json.ts => cloudformation-lang.ts} | 40 +++++++++++++++++-- packages/@aws-cdk/cdk/lib/construct.ts | 9 ++--- packages/@aws-cdk/cdk/lib/index.ts | 2 +- .../cdk/test/test.cloudformation-json.ts | 26 ++++++------ 5 files changed, 54 insertions(+), 52 deletions(-) delete mode 100644 packages/@aws-cdk/cdk/lib/cfn-concat.ts rename packages/@aws-cdk/cdk/lib/{cloudformation-json.ts => cloudformation-lang.ts} (67%) diff --git a/packages/@aws-cdk/cdk/lib/cfn-concat.ts b/packages/@aws-cdk/cdk/lib/cfn-concat.ts deleted file mode 100644 index 4b2d696acf098..0000000000000 --- a/packages/@aws-cdk/cdk/lib/cfn-concat.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Produce a CloudFormation expression to concat two arbitrary expressions when resolving - */ -export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { - if (left === undefined && right === undefined) { return ''; } - - const parts = new Array(); - if (left !== undefined) { parts.push(left); } - if (right !== undefined) { parts.push(right); } - - // Some case analysis to produce minimal expressions - if (parts.length === 1) { return parts[0]; } - if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { - return parts[0] + parts[1]; - } - - // Otherwise return a Join intrinsic (already in the target document language to avoid taking - // circular dependencies on FnJoin & friends) - return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; -} - -export class CloudFormationConcat implements IFragmentConcatenator { - public join(left: any, right: any) { - return cloudFormationConcat(left, right); - } -} - -import { IFragmentConcatenator } from "./encoding"; -import { minimalCloudFormationJoin } from "./instrinsics"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts similarity index 67% rename from packages/@aws-cdk/cdk/lib/cloudformation-json.ts rename to packages/@aws-cdk/cdk/lib/cloudformation-lang.ts index 9bb0d2296ab8d..a65eef23f4111 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts @@ -1,10 +1,11 @@ -import { isIntrinsic } from "./instrinsics"; +import { isIntrinsic, minimalCloudFormationJoin } from "./instrinsics"; +import { DefaultTokenResolver } from "./resolve"; import { IResolveContext, Token } from "./token"; /** - * Class for JSON routines that are framework-aware + * Routines that know how to do operations at the CloudFormation document language level */ -export class CloudFormationJSON { +export class CloudFormationLang { /** * Turn an arbitrary structure potentially containing Tokens into a JSON string. * @@ -18,7 +19,7 @@ export class CloudFormationJSON { * @param obj The object to stringify * @param context The Construct from which to resolve any Tokens found in the object */ - public static stringify(obj: any): string { + public static toJSON(obj: any): string { return new Token((ctx: IResolveContext) => { // Resolve inner value first so that if they evaluate to literals, we // maintain the type (and discard 'undefined's). @@ -63,6 +64,28 @@ export class CloudFormationJSON { return new IntrinsicToken(() => deepQuoteStringsForJSON(intrinsic)); } } + + /** + * Produce a CloudFormation expression to concat two arbitrary expressions when resolving + */ + public static concat(left: any | undefined, right: any | undefined): any { + if (left === undefined && right === undefined) { return ''; } + + const parts = new Array(); + if (left !== undefined) { parts.push(left); } + if (right !== undefined) { parts.push(right); } + + // Some case analysis to produce minimal expressions + if (parts.length === 1) { return parts[0]; } + if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { + return parts[0] + parts[1]; + } + + // Otherwise return a Join intrinsic (already in the target document language to avoid taking + // circular dependencies on FnJoin & friends) + return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; + } + } /** @@ -100,3 +123,12 @@ function deepQuoteStringsForJSON(x: any): any { return x; } + +/** + * Default Token resolver for CloudFormation templates + */ +export const CLOUDFORMATION_TOKEN_RESOLVER = new DefaultTokenResolver({ + join(left: any, right: any) { + return CloudFormationLang.concat(left, right); + } +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/construct.ts b/packages/@aws-cdk/cdk/lib/construct.ts index 6b45b536ec9fa..c771e05d9e8af 100644 --- a/packages/@aws-cdk/cdk/lib/construct.ts +++ b/packages/@aws-cdk/cdk/lib/construct.ts @@ -1,8 +1,8 @@ import cxapi = require('@aws-cdk/cx-api'); import { IAspect } from './aspect'; -import { CloudFormationJSON } from './cloudformation-json'; +import { CLOUDFORMATION_TOKEN_RESOLVER, CloudFormationLang } from './cloudformation-lang'; import { IDependable } from './dependency'; -import { DefaultTokenResolver, resolve } from './resolve'; +import { resolve } from './resolve'; import { Token } from './token'; import { makeUniqueId } from './uniqueid'; @@ -457,7 +457,7 @@ export class ConstructNode { return resolve(obj, { scope: this.host, prefix: [], - resolver: new DefaultTokenResolver(new CloudFormationConcat()), + resolver: CLOUDFORMATION_TOKEN_RESOLVER, }); } @@ -465,7 +465,7 @@ export class ConstructNode { * Convert an object, potentially containing tokens, to a JSON string */ public stringifyJson(obj: any): string { - return CloudFormationJSON.stringify(obj).toString(); + return CloudFormationLang.toJSON(obj).toString(); } /** @@ -726,5 +726,4 @@ export interface OutgoingReference { } // Import this _after_ everything else to help node work the classes out in the correct order... -import { CloudFormationConcat } from './cfn-concat'; import { Reference } from './reference'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 328863209ef19..151a8e592a0f5 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -7,7 +7,7 @@ export * from './token-map'; export * from './tag-manager'; export * from './dependency'; -export * from './cloudformation-json'; +export * from './cloudformation-lang'; export * from './reference'; export * from './cfn-condition'; export * from './fn'; diff --git a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts index 02b5242bdfc6d..81313b2d53faf 100644 --- a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationJSON, Fn, Stack, Token } from '../lib'; +import { CloudFormationLang, Fn, Stack, Token } from '../lib'; import { evaluateCFN } from './evaluate-cfn'; export = { @@ -23,7 +23,7 @@ export = { const fido = { name: 'Fido', speaks: token }; // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido)); + const resolved = stack.node.resolve(CloudFormationLang.toJSON(fido)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"woof woof"}'); @@ -40,7 +40,7 @@ export = { const fido = { name: 'Fido', speaks: `deep ${token}` }; // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido)); + const resolved = stack.node.resolve(CloudFormationLang.toJSON(fido)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"deep woof woof"}'); @@ -57,8 +57,8 @@ export = { // WHEN test.equal(evaluateCFN(stack.node.resolve(embedded)), "the number is 1"); - test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ embedded }))), "{\"embedded\":\"the number is 1\"}"); - test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ num }))), "{\"num\":1}"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationLang.toJSON({ embedded }))), "{\"embedded\":\"the number is 1\"}"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationLang.toJSON({ num }))), "{\"num\":1}"); test.done(); }, @@ -68,7 +68,7 @@ export = { const stack = new Stack(); for (const token of tokensThatResolveTo('pong!')) { // WHEN - const stringified = CloudFormationJSON.stringify(`ping? ${token}`); + const stringified = CloudFormationLang.toJSON(`ping? ${token}`); // THEN test.equal(evaluateCFN(stack.node.resolve(stringified)), '"ping? pong!"'); @@ -83,7 +83,7 @@ export = { const bucketName = new Token({ Ref: 'MyBucket' }); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: bucketName })); + const resolved = stack.node.resolve(CloudFormationLang.toJSON({ theBucket: bucketName })); // THEN const context = {MyBucket: 'TheName'}; @@ -108,7 +108,7 @@ export = { }, })); - const stringified = CloudFormationJSON.stringify(fakeIntrinsics); + const stringified = CloudFormationLang.toJSON(fakeIntrinsics); test.equal(evaluateCFN(stack.node.resolve(stringified)), '{"a":{"Fn::GetArtifactAtt":{"key":"val"}},"b":{"Fn::GetParam":["val1","val2"]}}'); @@ -121,7 +121,7 @@ export = { const token = Fn.join('', [ 'Hello', 'This\nIs', 'Very "cool"' ]); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationLang.toJSON({ literal: 'I can also "contain" quotes', token })); @@ -140,7 +140,7 @@ export = { const combinedName = Fn.join('', [ 'The bucket name is ', bucketName.toString() ]); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: combinedName })); + const resolved = stack.node.resolve(CloudFormationLang.toJSON({ theBucket: combinedName })); // THEN const context = {MyBucket: 'TheName'}; @@ -155,7 +155,7 @@ export = { const fidoSays = new Token(() => 'woof'); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationLang.toJSON({ information: `Did you know that Fido says: ${fidoSays}` })); @@ -171,7 +171,7 @@ export = { const fidoSays = new Token(() => ({ Ref: 'Something' })); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationLang.toJSON({ information: `Did you know that Fido says: ${fidoSays}` })); @@ -188,7 +188,7 @@ export = { const fidoSays = new Token(() => '"woof"'); // WHEN - const resolved = stack.node.resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationLang.toJSON({ information: `Did you know that Fido says: ${fidoSays}` })); From 2e717ad6ea502fa98e4eb1b39a0993782c6e17b3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 May 2019 11:23:02 +0200 Subject: [PATCH 06/33] WIP --- packages/@aws-cdk/aws-events/test/test.rule.ts | 1 - .../@aws-cdk/cdk/lib/cloudformation-lang.ts | 18 ++++++++++++++++++ packages/@aws-cdk/cdk/lib/token.ts | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index b0f5254bdba1b..bcaf041cfedaa 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -351,7 +351,6 @@ export = { test.done(); }, - 'rule can be disabled'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts index a65eef23f4111..8a4e2bd505b48 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts @@ -20,6 +20,24 @@ export class CloudFormationLang { * @param context The Construct from which to resolve any Tokens found in the object */ public static toJSON(obj: any): string { + // This works in two stages: + // + // First, resolve everything. This gets rid of the lazy evaluations, evaluation + // to the real types of things (for example, would a function return a string, an + // intrinsic, or a number? We have to resolve to know). + // + // We then to through the returned result, identify things that evaluated to + // CloudFormation intrinsics, and re-wrap those in Tokens that have a + // toJSON() method returning their string representation. If we then call + // JSON.stringify() on that result, that gives us essentially the same + // string that we started with, except with the non-token characters quoted. + // + // {"field": "${TOKEN}"} --> {\"field\": \"${TOKEN}\"} + // + // A final resolve() on that string (done by the framework) will yield the string + // we're after. + + return new Token((ctx: IResolveContext) => { // Resolve inner value first so that if they evaluate to literals, we // maintain the type (and discard 'undefined's). diff --git a/packages/@aws-cdk/cdk/lib/token.ts b/packages/@aws-cdk/cdk/lib/token.ts index 86974f303b241..4714fdc7f6546 100644 --- a/packages/@aws-cdk/cdk/lib/token.ts +++ b/packages/@aws-cdk/cdk/lib/token.ts @@ -105,6 +105,11 @@ export class Token { * it's not possible to do this properly, so we just throw an error here. */ public toJSON(): any { + // We can't do the right work here because in case we contain a function, we + // won't know the type of value that function represents (in the simplest + // case, string or number), and we can't know that without an + // IResolveContext to actually do the resolution, which we don't have. + // tslint:disable-next-line:max-line-length throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use this.node.stringifyJson() instead.'); } From 43e01d645b8e5368e2ddd0d872b9fa527fffb30c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 May 2019 11:46:20 +0200 Subject: [PATCH 07/33] Rewrite toJSON() in terms of new resolver mechanisms --- .../@aws-cdk/cdk/lib/cloudformation-lang.ts | 75 ++++++++----------- packages/@aws-cdk/cdk/lib/encoding.ts | 1 + packages/@aws-cdk/cdk/lib/resolve.ts | 16 ++-- 3 files changed, 38 insertions(+), 54 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts index 8a4e2bd505b48..6e8ed8b47a83f 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts @@ -1,5 +1,6 @@ +import { IFragmentConcatenator, TokenString } from "./encoding"; import { isIntrinsic, minimalCloudFormationJoin } from "./instrinsics"; -import { DefaultTokenResolver } from "./resolve"; +import { DefaultTokenResolver, resolve } from "./resolve"; import { IResolveContext, Token } from "./token"; /** @@ -36,50 +37,34 @@ export class CloudFormationLang { // // A final resolve() on that string (done by the framework) will yield the string // we're after. - - - return new Token((ctx: IResolveContext) => { - // Resolve inner value first so that if they evaluate to literals, we - // maintain the type (and discard 'undefined's). - // - // Then replace intrinsics with a special subclass of Token that - // overrides toJSON() to the marker string, so if we resolve() the - // strings again it evaluates to the right string. It also - // deep-escapes any strings inside the intrinsic, so that if literal - // strings are used in {Fn::Join} or something, they will end up - // escaped in the final JSON output. - const resolved = ctx.resolve(obj); - - // We can just directly return this value, since resolve() will be called - // on our return value anyway. - return JSON.stringify(deepReplaceIntrinsics(resolved)); - }).toString(); - - /** - * Recurse into a structure, replace all intrinsics with IntrinsicTokens. - */ - function deepReplaceIntrinsics(x: any): any { - if (x == null) { return x; } - - if (isIntrinsic(x)) { - return wrapIntrinsic(x); + // + // Resolving and wrapping are done in go using the resolver framework. + class IntrinsincWrapper extends DefaultTokenResolver { + constructor() { + super(CLOUDFORMATION_CONCAT); } - if (Array.isArray(x)) { - return x.map(deepReplaceIntrinsics); + public resolveToken(t: Token, context: IResolveContext) { + return wrap(super.resolveToken(t, context)); } - - if (typeof x === 'object') { - for (const key of Object.keys(x)) { - x[key] = deepReplaceIntrinsics(x[key]); - } + public resolveString(s: TokenString, context: IResolveContext) { + return wrap(super.resolveString(s, context)); + } + public resolveList(l: string[], context: IResolveContext) { + return wrap(super.resolveList(l, context)); } - - return x; } - function wrapIntrinsic(intrinsic: any): IntrinsicToken { - return new IntrinsicToken(() => deepQuoteStringsForJSON(intrinsic)); + // We need a ResolveContext to get started so return a Token + return new Token((ctx: IResolveContext) => { + return JSON.stringify(resolve(obj, { + scope: ctx.scope, + resolver: new IntrinsincWrapper() + })); + }).toString(); + + function wrap(value: any): any { + return isIntrinsic(value) ? new IntrinsicToken(() => deepQuoteStringsForJSON(value)) : value; } } @@ -142,11 +127,13 @@ function deepQuoteStringsForJSON(x: any): any { return x; } -/** - * Default Token resolver for CloudFormation templates - */ -export const CLOUDFORMATION_TOKEN_RESOLVER = new DefaultTokenResolver({ +const CLOUDFORMATION_CONCAT: IFragmentConcatenator = { join(left: any, right: any) { return CloudFormationLang.concat(left, right); } -}); \ No newline at end of file +}; + +/** + * Default Token resolver for CloudFormation templates + */ +export const CLOUDFORMATION_TOKEN_RESOLVER = new DefaultTokenResolver(CLOUDFORMATION_CONCAT); \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/encoding.ts b/packages/@aws-cdk/cdk/lib/encoding.ts index b7eb2e6ee2076..477e554979fec 100644 --- a/packages/@aws-cdk/cdk/lib/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/encoding.ts @@ -144,6 +144,7 @@ class TokenizedStringFragments { */ public join(concat: IFragmentConcatenator): any { if (this.fragments.length === 0) { return concat.join(undefined, undefined); } + if (this.fragments.length === 1) { return this.firstValue; } const values = this.fragments.map(fragmentValue); diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index 4ec2af1f5d992..18c83b8335fbd 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -18,7 +18,7 @@ const tokenMap = TokenMap.instance(); export interface IResolveOptions { scope: IConstruct; resolver: ITokenResolver; - prefix: string[]; + prefix?: string[]; } /** @@ -29,13 +29,14 @@ export interface IResolveOptions { * @param prefix Prefix key path components for diagnostics. */ export function resolve(obj: any, options: IResolveOptions): any { - const pathName = '/' + options.prefix.join('/'); + const prefix = options.prefix || []; + const pathName = '/' + prefix.join('/'); /** * Make a new resolution context */ function makeContext(appendPath?: string): IResolveContext { - const newPrefix = appendPath !== undefined ? options.prefix.concat([appendPath]) : options.prefix; + const newPrefix = appendPath !== undefined ? prefix.concat([appendPath]) : options.prefix; return { scope: options.scope, resolve(x: any) { return resolve(x, { ...options, prefix: newPrefix }); } @@ -43,7 +44,7 @@ export function resolve(obj: any, options: IResolveOptions): any { } // protect against cyclic references by limiting depth. - if (options.prefix.length > 200) { + if (prefix.length > 200) { throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); } @@ -198,12 +199,7 @@ export class DefaultTokenResolver implements ITokenResolver { */ public resolveString(s: TokenString, context: IResolveContext) { const fragments = s.split(tokenMap.lookupToken.bind(tokenMap)); - - // require() here to break cyclic dependencies - const ret = fragments.mapTokens(context.resolve).join(this.concat); - - // Recurse - return context.resolve(ret); + return fragments.mapTokens(context.resolve).join(this.concat); } public resolveList(xs: string[], context: IResolveContext) { From 32a56e75725f7435555d0394a7d453f2e2f82593 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 May 2019 13:40:28 +0200 Subject: [PATCH 08/33] Using new Token mechanisms to implement EventField substitution --- .../@aws-cdk/aws-events-targets/lib/sns.ts | 4 +- packages/@aws-cdk/aws-events/lib/input.ts | 158 ++++++++++++++++-- packages/@aws-cdk/aws-events/lib/rule.ts | 11 +- packages/@aws-cdk/aws-events/lib/target.ts | 28 +--- .../@aws-cdk/aws-events/test/test.rule.ts | 57 +++---- packages/@aws-cdk/cdk/lib/encoding.ts | 4 +- packages/@aws-cdk/cdk/lib/index.ts | 2 + .../cdk/test/test.cloudformation-json.ts | 14 ++ 8 files changed, 197 insertions(+), 81 deletions(-) diff --git a/packages/@aws-cdk/aws-events-targets/lib/sns.ts b/packages/@aws-cdk/aws-events-targets/lib/sns.ts index 9cbea70478564..166b70a397e3d 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sns.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sns.ts @@ -34,14 +34,14 @@ export class SnsTopic implements events.IEventRuleTarget { * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions */ - public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(rule: events.IEventRule): events.EventRuleTargetProperties { // deduplicated automatically this.topic.grantPublish(new iam.ServicePrincipal('events.amazonaws.com')); return { id: this.topic.node.id, arn: this.topic.topicArn, - input: this.props.message && this.props.message.toInputProperties(), + input: this.props.message && this.props.message.bind(rule), }; } } diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts index f282481ddc409..ae5bf6d474f7b 100644 --- a/packages/@aws-cdk/aws-events/lib/input.ts +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -1,10 +1,10 @@ -import { Token } from '@aws-cdk/cdk'; -import { EventRuleTargetInputProperties } from './target'; +import { CloudFormationLang, DefaultTokenResolver, IResolveContext, resolve, StringConcat, Token } from '@aws-cdk/cdk'; +import { IEventRule } from './rule-ref'; /** * The input to send to the event target */ -export class EventTargetInput { +export abstract class EventTargetInput { /** * Pass text to the event target * @@ -12,7 +12,7 @@ export class EventTargetInput { * matched event. */ public static fromText(text: string): EventTargetInput { - return new EventTargetInput({ input: JSON.stringify(text) }); + return new FieldAwareEventInput(text, false); } /** @@ -22,27 +22,151 @@ export class EventTargetInput { * matched event. */ public static fromObject(obj: any): EventTargetInput { - return new EventTargetInput({ input: JSON.stringify(obj) }); + return new FieldAwareEventInput(obj, true); } /** * Take the event target input from a path in the event JSON */ public static fromEventPath(path: string): EventTargetInput { - return new EventTargetInput({ inputPath: path }); + return new LiteralEventInput({ inputPath: path }); } - private constructor(private readonly props: EventRuleTargetInputProperties) { + protected constructor() { } /** * Return the input properties for this input object */ - public toInputProperties() { + public abstract bind(rule: IEventRule): EventRuleTargetInputProperties; +} + +/** + * The input properties for an event target + */ +export interface EventRuleTargetInputProperties { + /** + * Literal input to the target service (must be valid JSON) + */ + readonly input?: string; + + /** + * JsonPath to take input from the input event + */ + readonly inputPath?: string; + + /** + * Input template to insert paths map into + */ + readonly inputTemplate?: string; + + /** + * Paths map to extract values from event and insert into `inputTemplate` + */ + readonly inputPathsMap?: {[key: string]: string}; +} + +/** + * Event Input that is directly derived from the construct + */ +class LiteralEventInput extends EventTargetInput { + constructor(private readonly props: EventRuleTargetInputProperties) { + super(); + } + + /** + * Return the input properties for this input object + */ + public bind(_rule: IEventRule): EventRuleTargetInputProperties { return this.props; } } +/** + * Input object that can contain field replacements + * + * Evaluation is done in the bind() method because token resolution + * requires access to the construct tree. + * + * Multiple tokens that use the same path will use the same substitution + * key. + * + * One weird exception: if we're in object context, we MUST skip the quotes + * around the placeholder. I assume this is so once a trivial string replace is + * done later on by CWE, numbers are still numbers. + * + * So in string context: + * + * "this is a string with a " + * + * But in object context: + * + * "{ \"this is the\": }" + * + * To achieve the latter, we use the cooperation of the JSONification framework. + */ +class FieldAwareEventInput extends EventTargetInput { + constructor(private readonly input: any, private readonly objectScope: boolean) { + super(); + } + + public bind(rule: IEventRule): EventRuleTargetInputProperties { + Array.isArray(this.objectScope); + let fieldCounter = 0; + const pathToKey = new Map(); + const inputPathsMap: {[key: string]: string} = {}; + + function keyForField(f: EventField) { + const existing = pathToKey.get(f.path); + if (existing !== undefined) { return existing; } + + fieldCounter += 1; + const key = f.nameHint || `f${fieldCounter}`; + pathToKey.set(f.path, key); + return key; + } + + class EventFieldReplacer extends DefaultTokenResolver { + constructor() { + super(new StringConcat()); + } + + public resolveToken(t: Token, _context: IResolveContext) { + if (!isEventField(t)) { return t; } + + const key = keyForField(t); + if (inputPathsMap[key] && inputPathsMap[key] !== t.path) { + throw new Error(`Single key '${key}' is used for two different JSON paths: '${t.path}' and '${inputPathsMap[key]}'`); + } + inputPathsMap[key] = t.path; + + return `<${key}>`; + } + } + + console.log('resolving', this.input); + console.log('resolved ', resolve(this.input, { + scope: rule, + resolver: new EventFieldReplacer() + })); + + const resolved = CloudFormationLang.toJSON(resolve(this.input, { + scope: rule, + resolver: new EventFieldReplacer() + })); + + if (Object.keys(inputPathsMap).length === 0) { + // Nothing special, just return 'input' + return { input: resolved }; + } + + return { + inputTemplate: resolved, + inputPathsMap + }; + } +} + /** * Represents a field in the event pattern */ @@ -93,16 +217,18 @@ export class EventField extends Token { * Extract a custom JSON path from the event */ public static fromPath(path: string, nameHint?: string): string { - EventField.fieldCounter += 1; + return new EventField(path, nameHint).toString(); + } - nameHint = nameHint || `f${EventField.fieldCounter}`; + private constructor(public readonly path: string, public readonly nameHint?: string) { + super(() => path); - return new EventField(`<${nameHint}>`, path).toString(); + Object.defineProperty(this, EVENT_FIELD_SYMBOL, { value: true }); } +} - private static fieldCounter = 0; +function isEventField(x: any): x is EventField { + return typeof x === 'object' && x !== null && x[EVENT_FIELD_SYMBOL]; +} - private constructor(public readonly key: string, public readonly path: string) { - super(key); - } -} \ No newline at end of file +const EVENT_FIELD_SYMBOL = Symbol.for('@aws-cdk/aws-events.EventField'); diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 46950e93a625b..cfdba12464479 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -123,6 +123,7 @@ export class EventRule extends Resource implements IEventRule { if (!target) { return; } const targetProps = target.bind(this); + const inputProps = targetProps.input && targetProps.input.bind(this); // check if a target with this ID already exists if (this.targets.find(t => t.id === targetProps.id)) { @@ -131,11 +132,11 @@ export class EventRule extends Resource implements IEventRule { this.targets.push({ ...targetProps, - input: targetProps.input && targetProps.input.input, - inputPath: targetProps.input && targetProps.input.inputPath, - inputTransformer: targetProps.input && targetProps.input.inputTemplate !== undefined ? { - inputTemplate: targetProps.input.inputTemplate, - inputPathsMap: targetProps.input.inputPathsMap, + input: inputProps && inputProps.input, + inputPath: inputProps && inputProps.inputPath, + inputTransformer: inputProps && inputProps.inputTemplate !== undefined ? { + inputTemplate: inputProps.inputTemplate, + inputPathsMap: inputProps.inputPathsMap, } : undefined, }); } diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index 8e53610b6774b..42aa97b686755 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -1,4 +1,5 @@ import { CfnRule } from './events.generated'; +import { EventTargetInput } from './input'; import { IEventRule } from './rule-ref'; /** @@ -62,30 +63,5 @@ export interface EventRuleTargetProperties { * * @default the entire event */ - readonly input?: EventRuleTargetInputProperties; -} - -/** - * The input properties for an event target - */ -export interface EventRuleTargetInputProperties { - /** - * Literal input to the target service (must be valid JSON) - */ - readonly input?: string; - - /** - * JsonPath to take input from the input event - */ - readonly inputPath?: string; - - /** - * Input template to insert paths map into - */ - readonly inputTemplate?: string; - - /** - * Paths map to extract values from event and insert into `inputTemplate` - */ - readonly inputPathsMap?: {[key: string]: string}; + readonly input?: EventTargetInput; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 2a49dd06072d1..ef4a79a3f1e8f 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -167,7 +167,7 @@ export = { id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', - input: EventTargetInput.fromText(`This is ${EventField.fromPath('$.detail.bla', 'bla')}`).toInputProperties(), + input: EventTargetInput.fromText(`This is ${EventField.fromPath('$.detail.bla', 'bla')}`), }) }; @@ -220,7 +220,7 @@ export = { // a plain string should just be stringified (i.e. double quotes added and escaped) rule.addTarget({ bind: () => ({ - id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', input: EventTargetInput.fromText('Hello, "world"').toInputProperties() + id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', input: EventTargetInput.fromText('Hello, "world"') }) }); @@ -229,7 +229,7 @@ export = { rule.addTarget({ bind: () => ({ id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' }, - input: EventTargetInput.fromText(cdk.Fn.join('', [ 'a', 'b' ]).toString()).toInputProperties(), + input: EventTargetInput.fromText(cdk.Fn.join('', [ 'a', 'b' ]).toString()), }) }); @@ -237,16 +237,15 @@ export = { rule.addTarget({ bind: () => ({ id: 'T3', arn: 'ARN3', - input: EventTargetInput.fromObject({ foo: EventField.fromPath('$.detail.bar') }).toInputProperties() + input: EventTargetInput.fromObject({ foo: EventField.fromPath('$.detail.bar') }), }) }); - // tokens can also used for JSON templates, but that means escaping is - // the responsibility of the user. + // tokens can also used for JSON templates. rule.addTarget({ bind: () => ({ id: 'T4', arn: 'ARN4', - input: EventTargetInput.fromText(cdk.Fn.join(' ', ['"', 'hello', '\"world\"', '"']).toString()).toInputProperties(), + input: EventTargetInput.fromText(cdk.Fn.join(' ', ['hello', '"world"']).toString()), }) }); @@ -259,35 +258,33 @@ export = { "ScheduleExpression": "rate(1 minute)", "Targets": [ { - "Arn": "ARN2", - "Id": "T2", - "Input": "\"Hello, \\\"world\\\"\"", - "RoleArn": "IAM-ROLE-ARN" + "Arn": "ARN2", + "Id": "T2", + "Input": '"Hello, \\"world\\""', + "RoleArn": "IAM-ROLE-ARN" }, { - "Arn": "ARN1", - "Id": "T1", - "Input": "\"ab\"", - "KinesisParameters": { - "PartitionKeyPath": "partitionKeyPath" - } + "Arn": "ARN1", + "Id": "T1", + "Input": "\"ab\"", + "KinesisParameters": { + "PartitionKeyPath": "partitionKeyPath" + } }, { - "Arn": "ARN3", - "Id": "T3", - "InputTransformer": { - "InputPathsMap": { - "bar": "$.detail.bar" - }, - "InputTemplate": "{ \"foo\": }" - } + "Arn": "ARN3", + "Id": "T3", + "InputTransformer": { + "InputPathsMap": { + "f1": "$.detail.bar" + }, + "InputTemplate": "{\"foo\": }" + } }, { - "Arn": "ARN4", - "Id": "T4", - "InputTransformer": { - "InputTemplate": "\" hello \"world\" \"" - } + "Arn": "ARN4", + "Id": "T4", + "Input": '"hello \\"world\\""' } ] } diff --git a/packages/@aws-cdk/cdk/lib/encoding.ts b/packages/@aws-cdk/cdk/lib/encoding.ts index 477e554979fec..ab37a71eefabf 100644 --- a/packages/@aws-cdk/cdk/lib/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/encoding.ts @@ -85,7 +85,7 @@ type Fragment = LiteralFragment | TokenFragment | IntrinsicFragment; /** * Fragments of a string with markers */ -class TokenizedStringFragments { +export class TokenizedStringFragments { public readonly fragments = new Array(); public get firstFragment(): Fragment { @@ -206,7 +206,7 @@ export class NullConcat implements IFragmentConcatenator { * * Drops 'undefined's. */ -export class StringConat implements IFragmentConcatenator { +export class StringConcat implements IFragmentConcatenator { public join(left: any | undefined, right: any | undefined): any { if (left === undefined) { return right !== undefined ? `${right}` : undefined; } if (right === undefined) { return `${left}`; } diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 151a8e592a0f5..21dfe29838551 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -6,6 +6,8 @@ export * from './token'; export * from './token-map'; export * from './tag-manager'; export * from './dependency'; +export * from './resolve'; +export * from './encoding'; export * from './cloudformation-lang'; export * from './reference'; diff --git a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts index 81313b2d53faf..6167d2c5e771d 100644 --- a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts @@ -49,6 +49,20 @@ export = { test.done(); }, + 'constant string has correct amount of quotes applied'(test: Test) { + const stack = new Stack(); + + const inputString = 'Hello, "world"'; + + // WHEN + const resolved = stack.node.resolve(CloudFormationLang.toJSON(inputString)); + + // THEN + test.deepEqual(evaluateCFN(resolved), JSON.stringify(inputString)); + + test.done(); + }, + 'integer Tokens behave correctly in stringification and JSONification'(test: Test) { // GIVEN const stack = new Stack(); From e50265ad11fe86ecb2eb9989017cd3cb8362aa57 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 May 2019 15:26:51 +0200 Subject: [PATCH 09/33] Fix JSII complaints by reodering exports --- packages/@aws-cdk/aws-events/lib/input.ts | 15 ++++++++++- .../@aws-cdk/cdk/lib/cloudformation-lang.ts | 5 ++-- packages/@aws-cdk/cdk/lib/encoding.ts | 26 +------------------ packages/@aws-cdk/cdk/lib/index.ts | 1 - packages/@aws-cdk/cdk/lib/resolve.ts | 22 +++++++++++++--- 5 files changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts index ae5bf6d474f7b..f92996dce886b 100644 --- a/packages/@aws-cdk/aws-events/lib/input.ts +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -1,4 +1,4 @@ -import { CloudFormationLang, DefaultTokenResolver, IResolveContext, resolve, StringConcat, Token } from '@aws-cdk/cdk'; +import { CloudFormationLang, DefaultTokenResolver, IFragmentConcatenator, IResolveContext, resolve, Token } from '@aws-cdk/cdk'; import { IEventRule } from './rule-ref'; /** @@ -232,3 +232,16 @@ function isEventField(x: any): x is EventField { } const EVENT_FIELD_SYMBOL = Symbol.for('@aws-cdk/aws-events.EventField'); + +/** + * Converts all fragments to strings and concats those + * + * Drops 'undefined's. + */ +class StringConcat implements IFragmentConcatenator { + public join(left: any | undefined, right: any | undefined): any { + if (left === undefined) { return right !== undefined ? `${right}` : undefined; } + if (right === undefined) { return `${left}`; } + return `${left}${right}`; + } +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts index 6e8ed8b47a83f..83875397c8fba 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts @@ -1,6 +1,6 @@ -import { IFragmentConcatenator, TokenString } from "./encoding"; +import { TokenString } from "./encoding"; import { isIntrinsic, minimalCloudFormationJoin } from "./instrinsics"; -import { DefaultTokenResolver, resolve } from "./resolve"; +import { DefaultTokenResolver, IFragmentConcatenator, resolve } from "./resolve"; import { IResolveContext, Token } from "./token"; /** @@ -18,7 +18,6 @@ export class CloudFormationLang { * in CloudFormation will fail. * * @param obj The object to stringify - * @param context The Construct from which to resolve any Tokens found in the object */ public static toJSON(obj: any): string { // This works in two stages: diff --git a/packages/@aws-cdk/cdk/lib/encoding.ts b/packages/@aws-cdk/cdk/lib/encoding.ts index ab37a71eefabf..ac5a8287df285 100644 --- a/packages/@aws-cdk/cdk/lib/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/encoding.ts @@ -1,3 +1,4 @@ +import { IFragmentConcatenator } from "./resolve"; import { RESOLVE_METHOD, Token } from "./token"; // Details for encoding and decoding Tokens into native types; should not be exported @@ -177,18 +178,6 @@ function regexQuote(s: string) { return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); } -/** - * Function used to concatenate symbols in the target document language - * - * Interface so it could potentially be exposed over jsii. - */ -export interface IFragmentConcatenator { - /** - * Join the fragment on the left and on the right - */ - join(left: any | undefined, right: any | undefined): any; -} - /** * Concatenator that disregards the input * @@ -201,19 +190,6 @@ export class NullConcat implements IFragmentConcatenator { } } -/** - * Converts all fragments to strings and concats those - * - * Drops 'undefined's. - */ -export class StringConcat implements IFragmentConcatenator { - public join(left: any | undefined, right: any | undefined): any { - if (left === undefined) { return right !== undefined ? `${right}` : undefined; } - if (right === undefined) { return `${left}`; } - return `${left}${right}`; - } -} - export function containsListTokenElement(xs: any[]) { return xs.some(x => typeof(x) === 'string' && TokenString.forListToken(x).test()); } diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 21dfe29838551..802881d5a41a9 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -7,7 +7,6 @@ export * from './token-map'; export * from './tag-manager'; export * from './dependency'; export * from './resolve'; -export * from './encoding'; export * from './cloudformation-lang'; export * from './reference'; diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index 18c83b8335fbd..3bb65750a6297 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -1,5 +1,5 @@ import { IConstruct } from './construct'; -import { containsListTokenElement, IFragmentConcatenator, NullConcat, TokenString, unresolved } from "./encoding"; +import { containsListTokenElement, NullConcat, TokenString, unresolved } from "./encoding"; import { IResolveContext, isResolvedValuePostProcessor, RESOLVE_METHOD, Token } from "./token"; import { TokenMap } from './token-map'; @@ -168,6 +168,18 @@ export interface ITokenResolver { resolveList(l: string[], context: IResolveContext): any; } +/** + * Function used to concatenate symbols in the target document language + * + * Interface so it could potentially be exposed over jsii. + */ +export interface IFragmentConcatenator { + /** + * Join the fragment on the left and on the right + */ + join(left: any | undefined, right: any | undefined): any; +} + /** * Default resolver implementation */ @@ -231,19 +243,23 @@ export function findTokens(scope: IConstruct, fn: () => any): Token[] { // Swallow potential errors that might occur because we might not have validate()d. } - return Array.from(resolver.tokensSeen); + return resolver.tokens; } /** * Remember all Tokens encountered while resolving */ export class RememberingTokenResolver extends DefaultTokenResolver { - public readonly tokensSeen = new Set(); + private readonly tokensSeen = new Set(); public resolveToken(t: Token, context: IResolveContext) { this.tokensSeen.add(t); return super.resolveToken(t, context); } + + public get tokens(): Token[] { + return Array.from(this.tokensSeen); + } } /** From 4b1d82acb484a6632cb4ec11b4cb2479b2e8a4fb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 May 2019 15:44:01 +0200 Subject: [PATCH 10/33] Object-scoped path replacement works --- packages/@aws-cdk/aws-events/lib/input.ts | 53 +++++++++++++++---- .../@aws-cdk/aws-events/test/test.rule.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation-lang.ts | 1 - 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts index f92996dce886b..f0f5ef80bbbfa 100644 --- a/packages/@aws-cdk/aws-events/lib/input.ts +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -103,7 +103,8 @@ class LiteralEventInput extends EventTargetInput { * * "{ \"this is the\": }" * - * To achieve the latter, we use the cooperation of the JSONification framework. + * To achieve the latter, we postprocess the JSON string to remove the surrounding + * quotes by using a string replace. */ class FieldAwareEventInput extends EventTargetInput { constructor(private readonly input: any, private readonly objectScope: boolean) { @@ -126,6 +127,8 @@ class FieldAwareEventInput extends EventTargetInput { return key; } + const self = this; + class EventFieldReplacer extends DefaultTokenResolver { constructor() { super(new StringConcat()); @@ -140,16 +143,10 @@ class FieldAwareEventInput extends EventTargetInput { } inputPathsMap[key] = t.path; - return `<${key}>`; + return self.keyPlaceholder(key); } } - console.log('resolving', this.input); - console.log('resolved ', resolve(this.input, { - scope: rule, - resolver: new EventFieldReplacer() - })); - const resolved = CloudFormationLang.toJSON(resolve(this.input, { scope: rule, resolver: new EventFieldReplacer() @@ -161,12 +158,43 @@ class FieldAwareEventInput extends EventTargetInput { } return { - inputTemplate: resolved, + inputTemplate: this.unquoteKeyPlaceholders(resolved), inputPathsMap }; } + + /** + * Return a template placeholder for the given key + * + * In object scope we'll need to get rid of surrounding quotes later on, so + * return a bracing that's unlikely to occur naturally (like tokens). + */ + private keyPlaceholder(key: string) { + if (!this.objectScope) { return `<${key}>`; } + return UNLIKELY_OPENING_STRING + key + UNLIKELY_CLOSING_STRING; + } + + /** + * Removing surrounding quotes from any object placeholders + * + * Those have been put there by JSON.stringify(), but we need to + * remove them. + */ + private unquoteKeyPlaceholders(sub: string) { + if (!this.objectScope) { return sub; } + + return new Token((ctx: IResolveContext) => + ctx.resolve(sub).replace(OPENING_STRING_REGEX, '<').replace(CLOSING_STRING_REGEX, '>') + ).toString(); + } } +const UNLIKELY_OPENING_STRING = '<<${'; +const UNLIKELY_CLOSING_STRING = '}>>'; + +const OPENING_STRING_REGEX = new RegExp(regexQuote('"' + UNLIKELY_OPENING_STRING), 'g'); +const CLOSING_STRING_REGEX = new RegExp(regexQuote(UNLIKELY_CLOSING_STRING + '"'), 'g'); + /** * Represents a field in the event pattern */ @@ -245,3 +273,10 @@ class StringConcat implements IFragmentConcatenator { return `${left}${right}`; } } + +/** + * Quote a string for use in a regex + */ +function regexQuote(s: string) { + return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index ef4a79a3f1e8f..9ca6ebb4b78e4 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -278,7 +278,7 @@ export = { "InputPathsMap": { "f1": "$.detail.bar" }, - "InputTemplate": "{\"foo\": }" + "InputTemplate": "{\"foo\":}" } }, { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts index 83875397c8fba..aa49358910b27 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts @@ -87,7 +87,6 @@ export class CloudFormationLang { // circular dependencies on FnJoin & friends) return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; } - } /** From 2343be861fc1ebd3d78964aad1bee9aeae8df976 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 14 May 2019 08:57:11 +0200 Subject: [PATCH 11/33] Mass implement tasks --- packages/@aws-cdk/aws-codebuild/lib/events.ts | 88 ++++++++++ packages/@aws-cdk/aws-codebuild/lib/index.ts | 1 + .../@aws-cdk/aws-codebuild/lib/project.ts | 15 ++ .../@aws-cdk/aws-codecommit/lib/events.ts | 66 ++++++++ packages/@aws-cdk/aws-codecommit/lib/index.ts | 1 + .../test/integ.codecommit-events.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/index.ts | 1 - .../@aws-cdk/aws-events-targets/.gitignore | 3 +- .../aws-events-targets/lib/codebuild.ts | 32 +--- .../aws-events-targets/lib/ecs-ec2-task.ts | 87 +++++----- .../lib/ecs-task-properties.ts | 58 +++++++ .../@aws-cdk/aws-events-targets/lib/index.ts | 2 + .../@aws-cdk/aws-events-targets/lib/lambda.ts | 21 ++- .../@aws-cdk/aws-events-targets/lib/sns.ts | 4 +- .../@aws-cdk/aws-events-targets/package.json | 4 +- .../test/codebuild/codebuild.test.ts | 2 +- .../integ.project-events.expected.json | 118 +++++++------- .../test/codebuild/integ.project-events.ts | 22 ++- .../test/ecs/ec2-event-rule-target.test.ts | 57 +++++++ .../test/ecs}/eventhandler-image/Dockerfile | 0 .../test/ecs}/eventhandler-image/index.py | 0 .../integ.event-task.lit.expected.json | 150 +++++++++--------- .../test/{ => ecs}/integ.event-task.lit.ts | 27 ++-- .../test/test.ec2-event-rule-target.ts | 61 ------- packages/@aws-cdk/aws-events/lib/input.ts | 48 ++++-- packages/@aws-cdk/aws-events/lib/rule.ts | 30 +++- packages/@aws-cdk/aws-events/lib/target.ts | 8 +- .../test/test.input.ts} | 42 ++--- .../@aws-cdk/aws-events/test/test.rule.ts | 52 +++++- 29 files changed, 639 insertions(+), 363 deletions(-) create mode 100644 packages/@aws-cdk/aws-codebuild/lib/events.ts create mode 100644 packages/@aws-cdk/aws-codecommit/lib/events.ts create mode 100644 packages/@aws-cdk/aws-events-targets/lib/ecs-task-properties.ts create mode 100644 packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts rename packages/@aws-cdk/{aws-ecs/test => aws-events-targets/test/ecs}/eventhandler-image/Dockerfile (100%) rename packages/@aws-cdk/{aws-ecs/test => aws-events-targets/test/ecs}/eventhandler-image/index.py (100%) rename packages/@aws-cdk/aws-events-targets/test/{ => ecs}/integ.event-task.lit.expected.json (98%) rename packages/@aws-cdk/aws-events-targets/test/{ => ecs}/integ.event-task.lit.ts (71%) delete mode 100644 packages/@aws-cdk/aws-events-targets/test/test.ec2-event-rule-target.ts rename packages/@aws-cdk/{aws-events-targets/test/test.target-inputs.ts => aws-events/test/test.input.ts} (66%) diff --git a/packages/@aws-cdk/aws-codebuild/lib/events.ts b/packages/@aws-cdk/aws-codebuild/lib/events.ts new file mode 100644 index 0000000000000..8f781b0462c53 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/lib/events.ts @@ -0,0 +1,88 @@ +import events = require('@aws-cdk/aws-events'); + +/** + * Event fields for the CodeBuild "state change" event + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html#sample-build-notifications-ref + */ +export class StateChangeEvent { + /** + * The triggering build's status + */ + public static get buildStatus() { + return events.EventField.fromPath('$.detail.build-status'); + } + + /** + * The triggering build's project name + */ + public static get projectName() { + return events.EventField.fromPath('$.detail.project-name'); + } + + /** + * Return the build id + */ + public static get buildId() { + return events.EventField.fromPath('$.detail.build-id'); + } + + public static get currentPhase() { + return events.EventField.fromPath('$.detail.current-phase'); + } + + private constructor() { + } +} + +/** + * Event fields for the CodeBuild "phase change" event + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html#sample-build-notifications-ref + */ +export class PhaseChangeEvent { + /** + * The triggering build's project name + */ + public static get projectName() { + return events.EventField.fromPath('$.detail.project-name'); + } + + /** + * The triggering build's id + */ + public static get buildId() { + return events.EventField.fromPath('$.detail.build-id'); + } + + /** + * The phase that was just completed + */ + public static get completedPhase() { + return events.EventField.fromPath('$.detail.completed-phase'); + } + + /** + * The status of the completed phase + */ + public static get completedPhaseStatus() { + return events.EventField.fromPath('$.detail.completed-phase-status'); + } + + /** + * The duration of the completed phase + */ + public static get completedPhaseDurationSeconds() { + return events.EventField.fromPath('$.detail.completed-phase-duration-seconds'); + } + + /** + * Whether the build is complete + */ + public static get buildComplete() { + return events.EventField.fromPath('$.detail.build-complete'); + } + + private constructor() { + } +} diff --git a/packages/@aws-cdk/aws-codebuild/lib/index.ts b/packages/@aws-cdk/aws-codebuild/lib/index.ts index 7fbe814d26429..1382d4ad49d6f 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/index.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/index.ts @@ -1,3 +1,4 @@ +export * from './events'; export * from './pipeline-project'; export * from './project'; export * from './source'; diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 17610638cd83e..c8ff9744dc488 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -52,6 +52,9 @@ export interface IProject extends IResource, iam.IGrantable { * You can also use the methods `onBuildFailed` and `onBuildSucceeded` to define rules for * these specific state changes. * + * To access fields from the event in the event target input, + * use the static fields on the `StateChangeEvent` class. + * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html */ onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; @@ -195,6 +198,9 @@ abstract class ProjectBase extends Resource implements IProject { * You can also use the methods `onBuildFailed` and `onBuildSucceeded` to define rules for * these specific state changes. * + * To access fields from the event in the event target input, + * use the static fields on the `StateChangeEvent` class. + * * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html */ public onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { @@ -235,6 +241,9 @@ abstract class ProjectBase extends Resource implements IProject { /** * Defines an event rule which triggers when a build starts. + * + * 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.IEventRuleTarget, options?: events.EventRuleProps) { const rule = this.onStateChange(name, target, options); @@ -248,6 +257,9 @@ abstract class ProjectBase extends Resource implements IProject { /** * Defines an event rule which triggers when a build fails. + * + * 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.IEventRuleTarget, options?: events.EventRuleProps) { const rule = this.onStateChange(name, target, options); @@ -261,6 +273,9 @@ abstract class ProjectBase extends Resource implements IProject { /** * Defines an event rule which triggers when a build completes successfully. + * + * 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.IEventRuleTarget, options?: events.EventRuleProps) { const rule = this.onStateChange(name, target, options); diff --git a/packages/@aws-cdk/aws-codecommit/lib/events.ts b/packages/@aws-cdk/aws-codecommit/lib/events.ts new file mode 100644 index 0000000000000..e26842397591b --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/lib/events.ts @@ -0,0 +1,66 @@ +import events = require('@aws-cdk/aws-events'); + +/** + * Fields of CloudWatch Events that change references + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#codebuild_event_type + */ +export class ReferenceEvent { + /** + * The type of reference event + * + * 'referenceCreated', 'referenceUpdated' or 'referenceDeleted' + */ + public static get eventType() { + return events.EventField.fromPath('$.detail.event'); + } + + /** + * Name of the CodeCommit repository + */ + public static get repositoryName() { + return events.EventField.fromPath('$.detail.repositoryName'); + } + + /** + * Id of the CodeCommit repository + */ + public static get repositoryId() { + return events.EventField.fromPath('$.detail.repositoryId'); + } + + /** + * Type of reference changed + * + * 'branch' or 'tag' + */ + public static get referenceType() { + return events.EventField.fromPath('$.detail.referenceType'); + } + + /** + * Name of reference changed (branch or tag name) + */ + public static get referenceName() { + return events.EventField.fromPath('$.detail.referenceName'); + } + + /** + * Full reference name + * + * For example, 'refs/tags/myTag' + */ + public static get referenceFullName() { + return events.EventField.fromPath('$.detail.referenceFullName'); + } + + /** + * Commit id this reference now points to + */ + public static get commitId() { + return events.EventField.fromPath('$.detail.commitId'); + } + + private constructor() { + } +} diff --git a/packages/@aws-cdk/aws-codecommit/lib/index.ts b/packages/@aws-cdk/aws-codecommit/lib/index.ts index 2fa63e2e6ef94..05aa730eb214d 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/index.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/index.ts @@ -1,3 +1,4 @@ +export * from './events'; export * from './repository'; // AWS::CodeCommit CloudFormation Resources: 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 c5c6666cdae9b..b674b18656260 100644 --- a/packages/@aws-cdk/aws-codecommit/test/integ.codecommit-events.ts +++ b/packages/@aws-cdk/aws-codecommit/test/integ.codecommit-events.ts @@ -11,7 +11,7 @@ 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', { - asEventRuleTarget: () => ({ + bind: () => ({ arn: topic.topicArn, id: 'MyTopic' }) diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index a2a19ba7712ed..189f667461bbd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -9,7 +9,6 @@ export * from './placement'; export * from './ec2/ec2-service'; export * from './ec2/ec2-task-definition'; -export * from './ec2/ec2-event-rule-target'; export * from './fargate/fargate-service'; export * from './fargate/fargate-task-definition'; diff --git a/packages/@aws-cdk/aws-events-targets/.gitignore b/packages/@aws-cdk/aws-events-targets/.gitignore index 205e21fe7353b..0eb18b3fdfc39 100644 --- a/packages/@aws-cdk/aws-events-targets/.gitignore +++ b/packages/@aws-cdk/aws-events-targets/.gitignore @@ -13,4 +13,5 @@ lib/generated/resources.ts coverage .nycrc .LAST_PACKAGE -*.snk \ No newline at end of file +*.snk +.cdk.staging diff --git a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts index c013486673041..caee3c1040e49 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts @@ -6,42 +6,20 @@ import iam = require('@aws-cdk/aws-iam'); * Start a CodeBuild build when an AWS CloudWatch events rule is triggered. */ export class CodeBuildProject implements events.IEventRuleTarget { - constructor(private readonly project: codebuild.IProject) { - } /** * Allows using build projects as event rule targets. */ - public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { + public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { return { id: this.project.node.id, arn: this.project.projectArn, - roleArn: this.getCreateRole().roleArn, + policyStatements: [new iam.PolicyStatement() + .addAction('codebuild:StartBuild') + .addResource(this.project.projectArn) + ], }; } - - /** - * Gets or creates an IAM role associated with this CodeBuild project to allow - * CloudWatch Events to start builds for this project. - */ - private getCreateRole() { - const scope = this.project.node.stack; - const id = `@aws-cdk/aws-events-targets.CodeBuildProject:Role:${this.project.node.uniqueId}`; - const exists = scope.node.tryFindChild(id) as iam.Role; - if (exists) { - return exists; - } - - const role = new iam.Role(scope, id, { - assumedBy: new iam.ServicePrincipal('events.amazonaws.com') - }); - - role.addToPolicy(new iam.PolicyStatement() - .addAction('codebuild:StartBuild') - .addResource(this.project.projectArn)); - - return role; - } } diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts index fc203ebbb5ebd..f0b505a74db1e 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts @@ -1,22 +1,21 @@ +import ecs = require('@aws-cdk/aws-ecs'); import events = require ('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); -import cdk = require('@aws-cdk/cdk'); -import { TaskDefinition } from '../base/task-definition'; -import { ICluster } from '../cluster'; +import { ContainerOverride } from './ecs-task-properties'; /** * Properties to define an EC2 Event Task */ -export interface Ec2EventRuleTargetProps { +export interface EcsEc2TaskProps { /** * Cluster where service will be deployed */ - readonly cluster: ICluster; + readonly cluster: ecs.ICluster; /** * Task Definition of the task that should be started */ - readonly taskDefinition: TaskDefinition; + readonly taskDefinition: ecs.TaskDefinition; /** * How many tasks should be started when this event is triggered @@ -24,19 +23,25 @@ export interface Ec2EventRuleTargetProps { * @default 1 */ readonly taskCount?: number; + + /** + * Container setting overrides + * + * Key is the name of the container to override, value is the + * values you want to override. + */ + readonly containerOverrides?: ContainerOverride[]; } /** * Start a service on an EC2 cluster */ -export class Ec2EventRuleTarget extends cdk.Construct implements events.IEventRuleTarget { - private readonly cluster: ICluster; - private readonly taskDefinition: TaskDefinition; +export class EcsEc2Task implements events.IEventRuleTarget { + private readonly cluster: ecs.ICluster; + private readonly taskDefinition: ecs.TaskDefinition; private readonly taskCount: number; - constructor(scope: cdk.Construct, id: string, props: Ec2EventRuleTargetProps) { - super(scope, id); - + constructor(private readonly props: EcsEc2TaskProps) { if (!props.taskDefinition.isEc2Compatible) { throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); } @@ -49,52 +54,34 @@ export class Ec2EventRuleTarget extends cdk.Construct implements events.IEventRu /** * Allows using containers as target of CloudWatch events */ - public asEventRuleTarget(_ruleArn: string, _ruleUniqueId: string): events.EventRuleTargetProps { - const role = this.eventsRole; - - role.addToPolicy(new iam.PolicyStatement() + public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + const policyStatements = [new iam.PolicyStatement() .addAction('ecs:RunTask') .addResource(this.taskDefinition.taskDefinitionArn) - .addCondition('ArnEquals', { "ecs:cluster": this.cluster.clusterArn })); - - return { - id: this.node.id, - arn: this.cluster.clusterArn, - roleArn: role.roleArn, - ecsParameters: { - taskCount: this.taskCount, - taskDefinitionArn: this.taskDefinition.taskDefinitionArn - } - }; - } - - /** - * Create or get the IAM Role used to start this Task Definition. - * - * We create it under the TaskDefinition object so that if we have multiple EventTargets - * they can reuse the same role. - */ - public get eventsRole(): iam.IRole { - let role = this.taskDefinition.node.tryFindChild('EventsRole') as iam.IRole; - if (role === undefined) { - role = new iam.Role(this.taskDefinition, 'EventsRole', { - assumedBy: new iam.ServicePrincipal('events.amazonaws.com') - }); - } + .addCondition('ArnEquals', { "ecs:cluster": this.cluster.clusterArn }) + ]; - return role; - } - - /** - * Prepare the Event Rule Target - */ - protected prepare() { // If it so happens that a Task Execution Role was created for the TaskDefinition, // then the CloudWatch Events Role must have permissions to pass it (otherwise it doesn't). // // It never needs permissions to the Task Role. if (this.taskDefinition.executionRole !== undefined) { - this.taskDefinition.executionRole.grantPassRole(this.eventsRole); + policyStatements.push(new iam.PolicyStatement() + .addAction('iam:PassRole') + .addResource(this.taskDefinition.executionRole.roleArn)); } + + return { + id: this.taskDefinition.node.id + ' on ' + this.cluster.node.id, + arn: this.cluster.clusterArn, + policyStatements, + ecsParameters: { + taskCount: this.taskCount, + taskDefinitionArn: this.taskDefinition.taskDefinitionArn + }, + input: events.EventTargetInput.fromObject({ + containerOverrides: this.props.containerOverrides + }) + }; } } diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-task-properties.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-task-properties.ts new file mode 100644 index 0000000000000..11deb4cd8d8c5 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-task-properties.ts @@ -0,0 +1,58 @@ +export interface ContainerOverride { + /** + * Name of the container inside the task definition + */ + readonly containerName: string; + + /** + * Command to run inside the container + * + * @default Default command + */ + readonly command?: string[]; + + /** + * Variables to set in the container's environment + */ + readonly environment?: TaskEnvironmentVariable[]; + + /** + * The number of cpu units reserved for the container + * + * @default The default value from the task definition. + */ + readonly cpu?: number; + + /** + * Hard memory limit on the container + * + * @default The default value from the task definition. + */ + readonly memoryLimit?: number; + + /** + * Soft memory limit on the container + * + * @default The default value from the task definition. + */ + readonly memoryReservation?: number; +} + +/** + * An environment variable to be set in the container run as a task + */ +export interface TaskEnvironmentVariable { + /** + * Name for the environment variable + * + * Exactly one of `name` and `namePath` must be specified. + */ + readonly name: string; + + /** + * Value of the environment variable + * + * Exactly one of `value` and `valuePath` must be specified. + */ + readonly value: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index ee1e2d31b587e..7a729d238d0fe 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -1,3 +1,5 @@ export * from './sns'; export * from './codebuild'; export * from './lambda'; +export * from './ecs-task-properties'; +export * from './ecs-ec2-task'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts index 873cd84981388..44b1994a91b66 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts @@ -3,14 +3,24 @@ import iam = require('@aws-cdk/aws-iam'); import lambda = require('@aws-cdk/aws-lambda'); /** - * Use an AWS Lambda function as an event rule target. + * Customize the SNS Topic Event Target */ -export class LambdaFunction implements events.IEventRuleTarget { - +export interface LambdaFunctionProps { /** - * @param handler The lambda function + * The event to send to the Lambda + * + * This will be the payload sent to the Lambda Function. + * + * @default the entire CloudWatch event */ - constructor(private readonly handler: lambda.IFunction) { + event?: events.EventTargetInput; +} + +/** + * Use an AWS Lambda function as an event rule target. + */ +export class LambdaFunction implements events.IEventRuleTarget { + constructor(private readonly handler: lambda.IFunction, private readonly props: LambdaFunctionProps = {}) { } @@ -31,6 +41,7 @@ export class LambdaFunction implements events.IEventRuleTarget { return { id: this.handler.node.id, arn: this.handler.functionArn, + input: this.props.event, }; } } diff --git a/packages/@aws-cdk/aws-events-targets/lib/sns.ts b/packages/@aws-cdk/aws-events-targets/lib/sns.ts index 166b70a397e3d..af997a05ed7b2 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sns.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sns.ts @@ -34,14 +34,14 @@ export class SnsTopic implements events.IEventRuleTarget { * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions */ - public bind(rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { // deduplicated automatically this.topic.grantPublish(new iam.ServicePrincipal('events.amazonaws.com')); return { id: this.topic.node.id, arn: this.topic.topicArn, - input: this.props.message && this.props.message.bind(rule), + input: this.props.message, }; } } diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 169c2ebfda20f..0a775009208f8 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -48,7 +48,7 @@ ], "coverageThreshold": { "global": { - "branches": 80, + "branches": 70, "statements": 80 } } @@ -80,6 +80,7 @@ "dependencies": { "@aws-cdk/aws-codebuild": "^0.31.0", "@aws-cdk/aws-codepipeline": "^0.31.0", + "@aws-cdk/aws-ec2": "^0.31.0", "@aws-cdk/aws-ecs": "^0.31.0", "@aws-cdk/aws-events": "^0.31.0", "@aws-cdk/aws-iam": "^0.31.0", @@ -92,6 +93,7 @@ "peerDependencies": { "@aws-cdk/aws-codebuild": "^0.31.0", "@aws-cdk/aws-codepipeline": "^0.31.0", + "@aws-cdk/aws-ec2": "^0.31.0", "@aws-cdk/aws-ecs": "^0.31.0", "@aws-cdk/aws-events": "^0.31.0", "@aws-cdk/aws-iam": "^0.31.0", diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index c0d06ce165499..6f46fbbd07e6f 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -26,7 +26,7 @@ test('use codebuild project as an eventrule target', () => { Id: "MyProject", RoleArn: { "Fn::GetAtt": [ - "awscdkawseventstargetsCodeBuildProjectRoleMyProject02D63D81", + "ruleRoleMyProject4A80A7E9", "Arn" ] } diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json index f3255283328aa..6b3205bdc56f3 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json @@ -47,7 +47,7 @@ "Id": "MyProject", "RoleArn": { "Fn::GetAtt": [ - "awscdkawseventstargetsCodeBuildProjectRoleawscdkcodebuildeventsMyProjectEF919B0E11E20F6B", + "MyRepoOnCommitRoleMyProject127015C1", "Arn" ] } @@ -59,15 +59,68 @@ "Id": "MyTopic", "InputTransformer": { "InputPathsMap": { - "branch": "$.detail.referenceName", - "repo": "$.detail.repositoryName" + "f1": "$.detail.repositoryName", + "f2": "$.detail.referenceName" }, - "InputTemplate": "\"A commit was pushed to the repository on branch \"" + "InputTemplate": "\"A commit was pushed to the repository on branch \"" } } ] } }, + "MyRepoOnCommitRoleMyProject127015C1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "events.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyRepoOnCommitRoleMyProjectDefaultPolicy033A0114": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "codebuild:StartBuild", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProject39F7B0AE", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyRepoOnCommitRoleMyProjectDefaultPolicy033A0114", + "Roles": [ + { + "Ref": "MyRepoOnCommitRoleMyProject127015C1" + } + ] + } + }, "MyProjectRole9BBE5233": { "Type": "AWS::IAM::Role", "Properties": { @@ -263,9 +316,9 @@ "Id": "MyTopic", "InputTransformer": { "InputPathsMap": { - "phase": "$.detail.completed-phase" + "f1": "$.detail.completed-phase" }, - "InputTemplate": "\"Build phase changed to \"" + "InputTemplate": "\"Build phase changed to \"" } } ] @@ -362,59 +415,6 @@ } ] } - }, - "awscdkawseventstargetsCodeBuildProjectRoleawscdkcodebuildeventsMyProjectEF919B0E11E20F6B": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "events.", - { - "Ref": "AWS::URLSuffix" - } - ] - ] - } - } - } - ], - "Version": "2012-10-17" - } - } - }, - "awscdkawseventstargetsCodeBuildProjectRoleawscdkcodebuildeventsMyProjectEF919B0EDefaultPolicy03C827BE": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "codebuild:StartBuild", - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "MyProject39F7B0AE", - "Arn" - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "awscdkawseventstargetsCodeBuildProjectRoleawscdkcodebuildeventsMyProjectEF919B0EDefaultPolicy03C827BE", - "Roles": [ - { - "Ref": "awscdkawseventstargetsCodeBuildProjectRoleawscdkcodebuildeventsMyProjectEF919B0E11E20F6B" - } - ] - } } } } \ No newline at end of file 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 9f1886ead6fdc..aec1283fc9ffc 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 @@ -1,6 +1,7 @@ #!/usr/bin/env node import codebuild = require('@aws-cdk/aws-codebuild'); import codecommit = require('@aws-cdk/aws-codecommit'); +import events = require('@aws-cdk/aws-events'); import sns = require('@aws-cdk/aws-sns'); import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); @@ -27,21 +28,16 @@ project.onStateChange('StateChange', 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), { - textTemplate: `Build phase changed to `, - pathsMap: { - phase: '$.detail.completed-phase' - } -}); +project.onPhaseChange('PhaseChange').addTarget(new targets.SnsTopic(topic, { + message: events.EventTargetInput.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'); -onCommitRule.addTarget(new targets.SnsTopic(topic), { - textTemplate: 'A commit was pushed to the repository on branch ', - pathsMap: { - branch: '$.detail.referenceName', - repo: '$.detail.repositoryName' - } -}); +onCommitRule.addTarget(new targets.SnsTopic(topic, { + message: events.EventTargetInput.fromText( + `A commit was pushed to the repository ${codecommit.ReferenceEvent.repositoryName} on branch ${codecommit.ReferenceEvent.referenceName}` + ) +})); app.run(); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts new file mode 100644 index 0000000000000..1e7589cf1e92f --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts @@ -0,0 +1,57 @@ +import '@aws-cdk/assert/jest'; +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import events = require('@aws-cdk/aws-events'); +import cdk = require('@aws-cdk/cdk'); +import targets = require('../../lib'); + +test("Can use EC2 taskdef as EventRule target", () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') + }); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('henk'), + memoryLimitMiB: 256 + }); + + const rule = new events.EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)', + }); + + // WHEN + rule.addTarget(new targets.EcsEc2Task({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }] + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { "Fn::GetAtt": ["EcsCluster97242B84", "Arn"] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: { Ref: "TaskDef54694570" } + }, + InputTransformer: { + InputPathsMap: { + f1: "$.detail.event" + }, + InputTemplate: "{\"containerOverrides\":[{\"containerName\":\"TheContainer\",\"command\":[\"echo\",]}]}" + }, + RoleArn: { "Fn::GetAtt": ["RuleRoleTaskDefonEcsClusterE14C6DF4", "Arn"] } + } + ] + }); +}); diff --git a/packages/@aws-cdk/aws-ecs/test/eventhandler-image/Dockerfile b/packages/@aws-cdk/aws-events-targets/test/ecs/eventhandler-image/Dockerfile similarity index 100% rename from packages/@aws-cdk/aws-ecs/test/eventhandler-image/Dockerfile rename to packages/@aws-cdk/aws-events-targets/test/ecs/eventhandler-image/Dockerfile diff --git a/packages/@aws-cdk/aws-ecs/test/eventhandler-image/index.py b/packages/@aws-cdk/aws-events-targets/test/ecs/eventhandler-image/index.py similarity index 100% rename from packages/@aws-cdk/aws-ecs/test/eventhandler-image/index.py rename to packages/@aws-cdk/aws-events-targets/test/ecs/eventhandler-image/index.py diff --git a/packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json similarity index 98% rename from packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.expected.json rename to packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json index ed575becc83d6..ccc8f09fc184b 100644 --- a/packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json @@ -845,76 +845,6 @@ ] } }, - "TaskDefEventsRoleFB3B67B8": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "events.", - { - "Ref": "AWS::URLSuffix" - } - ] - ] - } - } - } - ], - "Version": "2012-10-17" - } - } - }, - "TaskDefEventsRoleDefaultPolicyA124E85B": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "ecs:RunTask", - "Condition": { - "ArnEquals": { - "ecs:cluster": { - "Fn::GetAtt": [ - "EcsCluster97242B84", - "Arn" - ] - } - } - }, - "Effect": "Allow", - "Resource": { - "Ref": "TaskDef54694570" - } - }, - { - "Action": "iam:PassRole", - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "TaskDefExecutionRoleB4775C97", - "Arn" - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "TaskDefEventsRoleDefaultPolicyA124E85B", - "Roles": [ - { - "Ref": "TaskDefEventsRoleFB3B67B8" - } - ] - } - }, "EventImageAdoptRepositoryDFAAC242": { "Type": "Custom::ECRAdoptedRepository", "Properties": { @@ -1121,19 +1051,87 @@ "Ref": "TaskDef54694570" } }, - "Id": "EventTarget", - "InputTransformer": { - "InputTemplate": "{\"containerOverrides\":[{\"name\":\"TheContainer\",\"environment\":[{\"name\":\"I_WAS_TRIGGERED\",\"value\":\"From CloudWatch Events\"}]}]}" - }, + "Id": "TaskDef-on-EcsCluster", + "Input": "{\"containerOverrides\":[{\"containerName\":\"TheContainer\",\"environment\":[{\"name\":\"I_WAS_TRIGGERED\",\"value\":\"From CloudWatch Events\"}]}]}", "RoleArn": { "Fn::GetAtt": [ - "TaskDefEventsRoleFB3B67B8", + "RuleRoleTaskDefonEcsClusterE14C6DF4", "Arn" ] } } ] } + }, + "RuleRoleTaskDefonEcsClusterE14C6DF4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "events.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RuleRoleTaskDefonEcsClusterDefaultPolicy8B2DC915": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecs:RunTask", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": { + "Ref": "TaskDef54694570" + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RuleRoleTaskDefonEcsClusterDefaultPolicy8B2DC915", + "Roles": [ + { + "Ref": "RuleRoleTaskDefonEcsClusterE14C6DF4" + } + ] + } } }, "Parameters": { @@ -1150,4 +1148,4 @@ "Description": "S3 key for asset version \"aws-ecs-integ-ecs/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.ts similarity index 71% rename from packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.ts rename to packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.ts index 3b6844b81c1fb..853504a3b68c9 100644 --- a/packages/@aws-cdk/aws-events-targets/test/integ.event-task.lit.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.ts @@ -1,7 +1,8 @@ import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); import events = require('@aws-cdk/aws-events'); import cdk = require('@aws-cdk/cdk'); -import ecs = require('../../lib'); +import targets = require('../../lib'); import path = require('path'); @@ -23,7 +24,7 @@ class EventStack extends cdk.Stack { const taskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef'); taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromAsset(this, 'EventImage', { - directory: path.resolve(__dirname, '..', 'eventhandler-image') + directory: path.resolve(__dirname, 'eventhandler-image') }), memoryLimitMiB: 256, logging: new ecs.AwsLogDriver(this, 'TaskLogging', { streamPrefix: 'EventDemo' }) @@ -35,21 +36,17 @@ class EventStack extends cdk.Stack { }); // Use Ec2TaskEventRuleTarget as the target of the EventRule - const target = new ecs.Ec2EventRuleTarget(this, 'EventTarget', { + rule.addTarget(new targets.EcsEc2Task({ cluster, taskDefinition, - taskCount: 1 - }); - - // Pass an environment variable to the container 'TheContainer' in the task - rule.addTarget(target, { - jsonTemplate: JSON.stringify({ - containerOverrides: [{ - name: 'TheContainer', - environment: [{ name: 'I_WAS_TRIGGERED', value: 'From CloudWatch Events' }] - }] - }) - }); + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + environment: [ + { name: 'I_WAS_TRIGGERED', value: 'From CloudWatch Events' } + ] + }] + })); /// !hide } } diff --git a/packages/@aws-cdk/aws-events-targets/test/test.ec2-event-rule-target.ts b/packages/@aws-cdk/aws-events-targets/test/test.ec2-event-rule-target.ts deleted file mode 100644 index f706a7a497735..0000000000000 --- a/packages/@aws-cdk/aws-events-targets/test/test.ec2-event-rule-target.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import ec2 = require('@aws-cdk/aws-ec2'); -import events = require('@aws-cdk/aws-events'); -import cdk = require('@aws-cdk/cdk'); -import { Test } from 'nodeunit'; -import ecs = require('../../lib'); - -export = { - "Can use EC2 taskdef as EventRule target"(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); - const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addCapacity('DefaultAutoScalingGroup', { - instanceType: new ec2.InstanceType('t2.micro') - }); - - const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); - taskDefinition.addContainer('TheContainer', { - image: ecs.ContainerImage.fromRegistry('henk'), - memoryLimitMiB: 256 - }); - - const rule = new events.EventRule(stack, 'Rule', { - scheduleExpression: 'rate(1 minute)', - }); - - // WHEN - const target = new ecs.Ec2EventRuleTarget(stack, 'EventTarget', { - cluster, - taskDefinition, - taskCount: 1 - }); - - rule.addTarget(target, { - jsonTemplate: { - argument: 'hello' - } - }); - - // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { - Targets: [ - { - Arn: { "Fn::GetAtt": ["EcsCluster97242B84", "Arn"] }, - EcsParameters: { - TaskCount: 1, - TaskDefinitionArn: { Ref: "TaskDef54694570" } - }, - Id: "EventTarget", - InputTransformer: { - InputTemplate: "{\"argument\":\"hello\"}" - }, - RoleArn: { "Fn::GetAtt": ["TaskDefEventsRoleFB3B67B8", "Arn"] } - } - ] - })); - - test.done(); - } -}; diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts index f0f5ef80bbbfa..66064258c1250 100644 --- a/packages/@aws-cdk/aws-events/lib/input.ts +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -12,7 +12,20 @@ export abstract class EventTargetInput { * matched event. */ public static fromText(text: string): EventTargetInput { - return new FieldAwareEventInput(text, false); + return new FieldAwareEventInput(text, InputType.Text); + } + + /** + * Pass text to the event target, splitting on newlines. + * + * This is only useful when passing to a target that does not + * take a single argument. + * + * May contain strings returned by EventField.from() to substitute in parts + * of the matched event. + */ + public static fromMultilineText(text: string): EventTargetInput { + return new FieldAwareEventInput(text, InputType.Multiline); } /** @@ -22,7 +35,7 @@ export abstract class EventTargetInput { * matched event. */ public static fromObject(obj: any): EventTargetInput { - return new FieldAwareEventInput(obj, true); + return new FieldAwareEventInput(obj, InputType.Object); } /** @@ -107,12 +120,11 @@ class LiteralEventInput extends EventTargetInput { * quotes by using a string replace. */ class FieldAwareEventInput extends EventTargetInput { - constructor(private readonly input: any, private readonly objectScope: boolean) { + constructor(private readonly input: any, private readonly inputType: InputType) { super(); } public bind(rule: IEventRule): EventRuleTargetInputProperties { - Array.isArray(this.objectScope); let fieldCounter = 0; const pathToKey = new Map(); const inputPathsMap: {[key: string]: string} = {}; @@ -147,10 +159,20 @@ class FieldAwareEventInput extends EventTargetInput { } } - const resolved = CloudFormationLang.toJSON(resolve(this.input, { - scope: rule, - resolver: new EventFieldReplacer() - })); + let resolved: string; + if (this.inputType === InputType.Multiline) { + // JSONify individual lines + resolved = resolve(this.input, { + scope: rule, + resolver: new EventFieldReplacer() + }); + resolved = resolved.split('\n').map(CloudFormationLang.toJSON).join('\n'); + } else { + resolved = CloudFormationLang.toJSON(resolve(this.input, { + scope: rule, + resolver: new EventFieldReplacer() + })); + } if (Object.keys(inputPathsMap).length === 0) { // Nothing special, just return 'input' @@ -170,7 +192,7 @@ class FieldAwareEventInput extends EventTargetInput { * return a bracing that's unlikely to occur naturally (like tokens). */ private keyPlaceholder(key: string) { - if (!this.objectScope) { return `<${key}>`; } + if (this.inputType !== InputType.Object) { return `<${key}>`; } return UNLIKELY_OPENING_STRING + key + UNLIKELY_CLOSING_STRING; } @@ -181,7 +203,7 @@ class FieldAwareEventInput extends EventTargetInput { * remove them. */ private unquoteKeyPlaceholders(sub: string) { - if (!this.objectScope) { return sub; } + if (this.inputType !== InputType.Object) { return sub; } return new Token((ctx: IResolveContext) => ctx.resolve(sub).replace(OPENING_STRING_REGEX, '<').replace(CLOSING_STRING_REGEX, '>') @@ -255,6 +277,12 @@ export class EventField extends Token { } } +enum InputType { + Object, + Text, + Multiline, +} + function isEventField(x: any): x is EventField { return typeof x === 'object' && x !== null && x[EVENT_FIELD_SYMBOL]; } diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index cfdba12464479..9cfd4b2daabda 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,3 +1,4 @@ +import iam = require('@aws-cdk/aws-iam'); import { CfnOutput, Construct, Resource, Token } from '@aws-cdk/cdk'; import { EventPattern } from './event-pattern'; import { CfnRule } from './events.generated'; @@ -123,15 +124,31 @@ export class EventRule extends Resource implements IEventRule { if (!target) { return; } const targetProps = target.bind(this); + const id = sanitizeId(targetProps.id); const inputProps = targetProps.input && targetProps.input.bind(this); // check if a target with this ID already exists - if (this.targets.find(t => t.id === targetProps.id)) { - throw new Error('Duplicate event rule target with ID: ' + targetProps.id); + if (this.targets.find(t => t.id === id)) { + throw new Error('Duplicate event rule target with ID: ' + id); + } + + let roleArn; + if ((targetProps.policyStatements || []).length > 0) { + const role = new iam.Role(this, `Role${targetProps.id}`, { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + }); + + for (const statement of targetProps.policyStatements || []) { + role.addToPolicy(statement); + } + + roleArn = role.roleArn; } this.targets.push({ ...targetProps, + id, + roleArn, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, inputTransformer: inputProps && inputProps.inputTemplate !== undefined ? { @@ -217,3 +234,12 @@ export class EventRule extends Resource implements IEventRule { return out; } } + +/** + * Sanitize whatever is returned to make a valid ID + * + * Result must match regex [\.\-_A-Za-z0-9]+ + */ +function sanitizeId(id: string) { + return id.replace(/[^\.\-_A-Za-z0-9]/g, '-'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index 42aa97b686755..6cca20a9623c0 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -1,3 +1,4 @@ +import iam = require('@aws-cdk/aws-iam'); import { CfnRule } from './events.generated'; import { EventTargetInput } from './input'; import { IEventRule } from './rule-ref'; @@ -32,12 +33,9 @@ export interface EventRuleTargetProperties { readonly arn: string; /** - * The Amazon Resource Name (ARN) of the AWS Identity and Access Management - * (IAM) role to use for this target when the rule is triggered. If one rule - * triggers multiple targets, you can use a different IAM role for each - * target. + * Policy statements to add to the event's role */ - readonly roleArn?: string; + readonly policyStatements?: iam.PolicyStatement[]; /** * The Amazon ECS task definition and task count to use, if the event target diff --git a/packages/@aws-cdk/aws-events-targets/test/test.target-inputs.ts b/packages/@aws-cdk/aws-events/test/test.input.ts similarity index 66% rename from packages/@aws-cdk/aws-events-targets/test/test.target-inputs.ts rename to packages/@aws-cdk/aws-events/test/test.input.ts index 1a85b998db3cf..e629a88ad4333 100644 --- a/packages/@aws-cdk/aws-events-targets/test/test.target-inputs.ts +++ b/packages/@aws-cdk/aws-events/test/test.input.ts @@ -1,8 +1,9 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; -import { EventRule, IEventRuleTarget } from '@aws-cdk/aws-events'; import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; +import { EventTargetInput, IEventRuleTarget } from '../lib'; +import { EventRule } from '../lib/rule'; export = { 'json template': { @@ -14,17 +15,13 @@ export = { }); // WHEN - rule.addTarget(new SomeTarget(), { - jsonTemplate: { SomeObject: 'withAValue' }, - }); + rule.addTarget(new SomeTarget(EventTargetInput.fromObject({ SomeObject: 'withAValue' }))); // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { Targets: [ { - InputTransformer: { - InputTemplate: "{\"SomeObject\":\"withAValue\"}" - }, + Input: "{\"SomeObject\":\"withAValue\"}" } ] })); @@ -41,17 +38,13 @@ export = { }); // WHEN - rule.addTarget(new SomeTarget(), { - textTemplate: 'I have\nmultiple lines', - }); + rule.addTarget(new SomeTarget(EventTargetInput.fromMultilineText('I have\nmultiple lines'))); // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { Targets: [ { - InputTransformer: { - InputTemplate: "\"I have\"\n\"multiple lines\"" - }, + Input: "\"I have\"\n\"multiple lines\"" } ] })); @@ -67,17 +60,13 @@ export = { }); // WHEN - rule.addTarget(new SomeTarget(), { - textTemplate: 'this is not\\na real newline', - }); + rule.addTarget(new SomeTarget(EventTargetInput.fromMultilineText('this is not\\na real newline'))), // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { Targets: [ { - InputTransformer: { - InputTemplate: "\"this is not\\\\na real newline\"" - }, + Input: "\"this is not\\\\na real newline\"" } ] })); @@ -95,17 +84,13 @@ export = { const world = new cdk.Token(() => 'world'); // WHEN - rule.addTarget(new SomeTarget(), { - textTemplate: `hello ${world}`, - }); + rule.addTarget(new SomeTarget(EventTargetInput.fromText(`hello ${world}`))); // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { Targets: [ { - InputTransformer: { - InputTemplate: "\"hello world\"" - }, + Input: "\"hello world\"" } ] })); @@ -116,9 +101,10 @@ export = { }; class SomeTarget implements IEventRuleTarget { + public constructor(private readonly input: EventTargetInput) { + } + public bind() { - return { - id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' } - }; + return { id: 'T1', arn: 'ARN1', input: this.input }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 9ca6ebb4b78e4..f3c4ba93a5e0b 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -1,4 +1,5 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; @@ -166,7 +167,6 @@ export = { bind: () => ({ id: 'T2', arn: 'ARN2', - roleArn: 'IAM-ROLE-ARN', input: EventTargetInput.fromText(`This is ${EventField.fromPath('$.detail.bla', 'bla')}`), }) }; @@ -202,7 +202,6 @@ export = { }, "InputTemplate": "\"This is \"" }, - "RoleArn": "IAM-ROLE-ARN" } ] } @@ -220,7 +219,7 @@ export = { // a plain string should just be stringified (i.e. double quotes added and escaped) rule.addTarget({ bind: () => ({ - id: 'T2', arn: 'ARN2', roleArn: 'IAM-ROLE-ARN', input: EventTargetInput.fromText('Hello, "world"') + id: 'T2', arn: 'ARN2', input: EventTargetInput.fromText('Hello, "world"') }) }); @@ -261,7 +260,6 @@ export = { "Arn": "ARN2", "Id": "T2", "Input": '"Hello, \\"world\\""', - "RoleArn": "IAM-ROLE-ARN" }, { "Arn": "ARN1", @@ -295,6 +293,50 @@ export = { test.done(); }, + 'target can declare policy statements which will be added to role'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const rule = new EventRule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); + + // a plain string should just be stringified (i.e. double quotes added and escaped) + rule.addTarget({ + bind: () => ({ + id: 'T2', + arn: 'ARN2', + policyStatements: [new iam.PolicyStatement() + .addAction('some:action') + .addResource('ARN2')] + }) + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + "Targets": [ + { + "Arn": "ARN2", + "Id": "T2", + "RoleArn": {"Fn::GetAtt": ["EventRuleRoleT2D4DAD5B9", "Arn"]} + } + ] + })); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "some:action", + "Effect": "Allow", + "Resource": "ARN2" + } + ], + "Version": "2012-10-17" + }, + })); + + test.done(); + }, + 'asEventRuleTarget can use the ruleArn and a uniqueId of the rule'(test: Test) { const stack = new cdk.Stack(); From ef204acad10c417594b370086efe6e7f12488219 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 14 May 2019 11:15:07 +0200 Subject: [PATCH 12/33] Fix some more build issues --- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 32 +++++++++++++++++-- packages/@aws-cdk/aws-cloudtrail/package.json | 2 ++ .../lib/codecommit/source-action.ts | 3 +- .../lib/ecr/source-action.ts | 3 +- .../lib/s3/source-action.ts | 3 +- .../cloudformation/test.pipeline-actions.ts | 2 +- .../test/integ.pipeline-events.ts | 13 ++++---- .../@aws-cdk/aws-codepipeline/lib/action.ts | 2 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 32 ------------------- .../aws-events-targets/lib/codepipeline.ts | 21 ++++++++++++ .../@aws-cdk/aws-events-targets/lib/index.ts | 1 + 11 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 398be10eb39be..954a204e4a53b 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -1,3 +1,4 @@ +import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import logs = require('@aws-cdk/aws-logs'); @@ -181,7 +182,7 @@ export class CloudTrail extends cdk.Construct { * @param prefixes the list of object ARN prefixes to include in logging (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addS3EventSelector(prefixes: string[], options: AddS3EventSelectorOptions = {}) { + public addS3EventSelector(prefixes: string[] = [], options: AddS3EventSelectorOptions = {}) { if (prefixes.length > 250) { throw new Error("A maximum of 250 data elements can be in one event selector"); } @@ -190,13 +191,38 @@ export class CloudTrail extends cdk.Construct { } this.eventSelectors.push({ includeManagementEvents: options.includeManagementEvents, - readWriteType: options.readWriteType, + readWriteType: options.readWriteType || ReadWriteType.WriteOnly, dataResources: [{ type: "AWS::S3::Object", values: prefixes }] }); } + + /** + * Create an event rule for when an event is added to this trail + */ + public onEvent(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { + const rule = new events.EventRule(this, name, options); + rule.addTarget(target); + rule.addEventPattern({ + detailType: ['AWS API Call via CloudTrail'] + }); + return rule; + } + + /** + * Create an event rule for when an S3 event is added to this trail + */ + public onS3Event(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { + const rule = new events.EventRule(this, name, options); + rule.addTarget(target); + rule.addEventPattern({ + source: ['aws.s3'], + detailType: ['AWS API Call via CloudTrail'] + }); + return rule; + } } /** @@ -206,7 +232,7 @@ export interface AddS3EventSelectorOptions { /** * Specifies whether to log read-only events, write-only events, or all events. * - * @default ReadWriteType.All + * @default ReadWriteType.Write */ readonly readWriteType?: ReadWriteType; diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index a69debeb4a5a7..9600940726213 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -67,6 +67,7 @@ "pkglint": "^0.31.0" }, "dependencies": { + "@aws-cdk/aws-events": "^0.31.0", "@aws-cdk/aws-iam": "^0.31.0", "@aws-cdk/aws-kms": "^0.31.0", "@aws-cdk/aws-logs": "^0.31.0", @@ -75,6 +76,7 @@ }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-events": "^0.31.0", "@aws-cdk/aws-iam": "^0.31.0", "@aws-cdk/aws-kms": "^0.31.0", "@aws-cdk/aws-logs": "^0.31.0", 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 ed52a1ed06b03..741a1478446ce 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 @@ -1,5 +1,6 @@ import codecommit = require('@aws-cdk/aws-codecommit'); import codepipeline = require('@aws-cdk/aws-codepipeline'); +import targets = require('@aws-cdk/aws-events-targets'); import iam = require('@aws-cdk/aws-iam'); import { sourceArtifactBounds } from '../common'; @@ -57,7 +58,7 @@ 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', - info.pipeline, this.props.branch || 'master'); + new targets.CodePipeline(info.pipeline), 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 cb721bd20884c..e86c3bed59739 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 @@ -1,5 +1,6 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import ecr = require('@aws-cdk/aws-ecr'); +import targets = require('@aws-cdk/aws-events-targets'); import iam = require('@aws-cdk/aws-iam'); import { sourceArtifactBounds } from '../common'; @@ -55,6 +56,6 @@ export class EcrSourceAction extends codepipeline.Action { .addResource(this.props.repository.repositoryArn)); this.props.repository.onImagePushed(info.pipeline.node.uniqueId + 'SourceEventRule', - info.pipeline, this.props.imageTag); + new targets.CodePipeline(info.pipeline), 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 16d8aa696b007..88f003dd29732 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 @@ -1,4 +1,5 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); +import targets = require('@aws-cdk/aws-events-targets'); import s3 = require('@aws-cdk/aws-s3'); import { sourceArtifactBounds } from '../common'; @@ -61,7 +62,7 @@ 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', - info.pipeline, this.props.bucketKey); + new targets.CodePipeline(info.pipeline), 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 8a4596d6a2e49..bb436b35d6889 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 @@ -313,7 +313,7 @@ class PipelineDouble extends cdk.Construct implements codepipeline.IPipeline { this.role = role; } - public asEventRuleTarget(_ruleArn: string, _ruleUniqueId: string): events.EventRuleTargetProps { + public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { throw new Error('asEventRuleTarget() is unsupported in PipelineDouble'); } 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 a79555c7fd883..eb1e40e1930da 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 @@ -3,6 +3,7 @@ import codebuild = require('@aws-cdk/aws-codebuild'); import codecommit = require('@aws-cdk/aws-codecommit'); import codepipeline = require('@aws-cdk/aws-codepipeline'); +import events = require('@aws-cdk/aws-events'); import targets = require('@aws-cdk/aws-events-targets'); import sns = require('@aws-cdk/aws-sns'); import cdk = require('@aws-cdk/cdk'); @@ -45,13 +46,11 @@ pipeline.addStage({ const topic = new sns.Topic(stack, 'MyTopic'); -pipeline.onStateChange('OnPipelineStateChange').addTarget(new targets.SnsTopic(topic), { - textTemplate: 'Pipeline changed state to ', - pathsMap: { - pipeline: '$.detail.pipeline', - state: '$.detail.state' - } -}); +const eventPipeline = events.EventField.fromPath('$.detail.pipeline'); +const eventState = events.EventField.fromPath('$.detail.state'); +pipeline.onStateChange('OnPipelineStateChange').addTarget(new targets.SnsTopic(topic, { + message: events.EventTargetInput.fromText(`Pipeline ${eventPipeline} changed state to ${eventState}`), +})); sourceStage.onStateChange('OnSourceStateChange', new targets.SnsTopic(topic)); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/action.ts b/packages/@aws-cdk/aws-codepipeline/lib/action.ts index 0d1e505d3f2cd..ef298860331ee 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/action.ts @@ -58,7 +58,7 @@ export interface ActionBind { * It extends {@link events.IEventRuleTarget}, * so this interface can be used as a Target for CloudWatch Events. */ -export interface IPipeline extends IResource, events.IEventRuleTarget { +export interface IPipeline extends IResource { /** * The name of the Pipeline. * diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 5805597bc5a31..75886cff7a8c1 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -101,41 +101,9 @@ export interface PipelineProps { abstract class PipelineBase extends Resource implements IPipeline { public abstract pipelineName: string; public abstract pipelineArn: string; - private eventsRole?: iam.Role; public abstract grantBucketRead(identity: iam.IGrantable): iam.Grant; public abstract grantBucketReadWrite(identity: iam.IGrantable): iam.Grant; - /** - * Allows the pipeline to be used as a CloudWatch event rule target. - * - * Usage: - * - * const pipeline = new Pipeline(this, 'MyPipeline'); - * const rule = new EventRule(this, 'MyRule', { schedule: 'rate(1 minute)' }); - * rule.addTarget(pipeline); - * - */ - public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { - // the first time the event rule target is retrieved, we define an IAM - // role assumable by the CloudWatch events service which is allowed to - // start the execution of this pipeline. no need to define more than one - // role per pipeline. - if (!this.eventsRole) { - this.eventsRole = new iam.Role(this, 'EventsRole', { - assumedBy: new iam.ServicePrincipal('events.amazonaws.com') - }); - - this.eventsRole.addToPolicy(new iam.PolicyStatement() - .addResource(this.pipelineArn) - .addAction('codepipeline:StartPipelineExecution')); - } - - return { - id: this.node.id, - arn: this.pipelineArn, - roleArn: this.eventsRole.roleArn, - }; - } } /** diff --git a/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts new file mode 100644 index 0000000000000..34b746fd702d2 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts @@ -0,0 +1,21 @@ +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import events = require('@aws-cdk/aws-events'); +import iam = require('@aws-cdk/aws-iam'); + + /** + * Allows the pipeline to be used as a CloudWatch event rule target. + */ +export class CodePipeline implements events.IEventRuleTarget { + constructor(private readonly pipeline: codepipeline.IPipeline) { + } + + public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + return { + id: this.pipeline.node.id, + arn: this.pipeline.pipelineArn, + policyStatements: [new iam.PolicyStatement() + .addResource(this.pipeline.pipelineArn) + .addAction('codepipeline:StartPipelineExecution')] + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index 7a729d238d0fe..529a9e27cd0de 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -1,3 +1,4 @@ +export * from './codepipeline'; export * from './sns'; export * from './codebuild'; export * from './lambda'; From 08a444a717b6e5f17c0a3b95b5d18a60399ec5f6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 14 May 2019 11:53:25 +0200 Subject: [PATCH 13/33] Improve error message for string key resolve --- packages/@aws-cdk/cdk/lib/resolve.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index 3bb65750a6297..748d9350be04b 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -130,7 +130,7 @@ export function resolve(obj: any, options: IResolveOptions): any { for (const key of Object.keys(obj)) { const resolvedKey = makeContext().resolve(key); if (typeof(resolvedKey) !== 'string') { - throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); + throw new Error(`"${key}" is used as the key in a map so must resolve to a string, but it resolves to: ${JSON.stringify(resolvedKey)}`); } const value = makeContext(key).resolve(obj[key]); From 46725ebe84130806d2313801d566722d31992dad Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 15 May 2019 13:46:37 +0200 Subject: [PATCH 14/33] Fix a bad bug about cross-stack references in a lazy context --- .../aws-events-targets/lib/ecs-ec2-task.ts | 2 + packages/@aws-cdk/aws-events/lib/rule.ts | 2 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 25 ++++++++- packages/@aws-cdk/cdk/lib/cfn-element.ts | 4 +- packages/@aws-cdk/cdk/lib/cfn-reference.ts | 56 ++++++++++++++++++- packages/@aws-cdk/cdk/lib/cfn-resource.ts | 5 +- packages/@aws-cdk/cdk/lib/pseudo.ts | 22 +++----- packages/@aws-cdk/cdk/lib/resolve.ts | 6 +- packages/@aws-cdk/cdk/test/test.stack.ts | 30 ++++++++++ 9 files changed, 123 insertions(+), 29 deletions(-) diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts index f0b505a74db1e..c6aa003fe59d3 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts @@ -31,6 +31,8 @@ export interface EcsEc2TaskProps { * values you want to override. */ readonly containerOverrides?: ContainerOverride[]; + + // AWS VPC configuration } /** diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 9cfd4b2daabda..3cc6801d3068b 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -92,7 +92,7 @@ export class EventRule extends Resource implements IEventRule { state: props.enabled == null ? 'ENABLED' : (props.enabled ? 'ENABLED' : 'DISABLED'), scheduleExpression: new Token(() => this.scheduleExpression).toString(), eventPattern: new Token(() => this.renderEventPattern()), - targets: new Token(() => this.renderTargets()) + targets: new Token(() => this.renderTargets()), }); this.ruleArn = resource.ruleArn; diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index ae1c6e35d517b..8399cb0c3c9aa 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -2,7 +2,7 @@ import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); -import { Stack } from '@aws-cdk/cdk'; +import { Stack, App, CfnOutput } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sqs = require('../lib'); import { Queue } from '../lib'; @@ -514,7 +514,28 @@ export = { }); test.done(); - } + }, + + 'reference stack from other stack'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const queue = new sqs.Queue(stack1, 'Queue'); + + const stack2 = new Stack(app, 'Stack2'); + + // WHEN + new CfnOutput(stack2, 'Output', { value: queue.queueArn }); + + // THEN + expect(stack2).toMatch({ + Outputs: { + Hello: 'bye' + } + }); + + test.done(); + }, }; function testGrant(action: (q: Queue, principal: iam.IPrincipal) => void, ...expectedActions: string[]) { diff --git a/packages/@aws-cdk/cdk/lib/cfn-element.ts b/packages/@aws-cdk/cdk/lib/cfn-element.ts index 8d728fc07cdf8..48cafcfbae780 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-element.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-element.ts @@ -147,8 +147,8 @@ export abstract class CfnRefElement extends CfnElement { /** * Return a token that will CloudFormation { Ref } this stack element */ - protected get referenceToken(): Token { - return new CfnReference({ Ref: this.logicalId }, 'Ref', this); + public get referenceToken(): Token { + return CfnReference.for(this, 'Ref'); } } diff --git a/packages/@aws-cdk/cdk/lib/cfn-reference.ts b/packages/@aws-cdk/cdk/lib/cfn-reference.ts index 0cb430a382058..4589116757e22 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-reference.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-reference.ts @@ -24,6 +24,55 @@ export class CfnReference extends Reference { return (x as any)[CFN_REFERENCE_SYMBOL] === true; } + /** + * Return the CfnReference for the indicated target + * + * Will make sure that multiple invocations for the same target and intrinsic + * return the same CfnReference. Because CfnReferences accumulate state in + * the prepare() phase (for the purpose of cross-stack references), it's + * important that the state isn't lost if it's lazily created, like so: + * + * new Token(() => new CfnReference(...)) + */ + public static for(target: CfnRefElement, attribute: string) { + return CfnReference.singletonReference(target, attribute, () => { + const cfnInstrinsic = attribute === 'Ref' ? { Ref: target.logicalId } : { 'Fn::GetAtt': [ target.logicalId, attribute ]}; + return new CfnReference(cfnInstrinsic, attribute, target); + }); + } + + /** + * Return a CfnReference that references a pseudo referencd + */ + public static forPseudo(pseudoName: string, scope: Construct) { + return CfnReference.singletonReference(scope, `Pseudo:${pseudoName}`, () => { + const cfnInstrinsic = { Ref: pseudoName }; + return new CfnReference(cfnInstrinsic, pseudoName, scope); + }); + } + + /** + * Static table where we keep singleton CfnReference instances + */ + private static referenceTable = new Map>(); + + /** + * Get or create the table + */ + private static singletonReference(target: Construct, attribKey: string, fresh: () => CfnReference) { + let attribs = CfnReference.referenceTable.get(target); + if (!attribs) { + attribs = new Map(); + CfnReference.referenceTable.set(target, attribs); + } + let ref = attribs.get(attribKey); + if (!ref) { + ref = fresh(); + attribs.set(attribKey, ref); + } + return ref; + } + /** * What stack this Token is pointing to */ @@ -36,9 +85,9 @@ export class CfnReference extends Reference { private readonly originalDisplayName: string; - constructor(value: any, displayName: string, target: Construct) { + private constructor(value: any, displayName: string, target: Construct) { if (typeof(value) === 'function') { - throw new Error('Reference can only hold CloudFormation intrinsics (not a function)'); + throw new Error('Reference can only hold CloudFormation intrinsics (not a function)'); } // prepend scope path to display name super(value, `${target.node.id}.${displayName}`, target); @@ -64,6 +113,7 @@ export class CfnReference extends Reference { * Register a stack this references is being consumed from. */ public consumeFromStack(consumingStack: Stack, consumingConstruct: IConstruct) { + // tslint:disable-next-line:max-line-length if (this.producingStack && this.producingStack !== consumingStack && !this.replacementTokens.has(consumingStack)) { // We're trying to resolve a cross-stack reference consumingStack.addDependency(this.producingStack, `${consumingConstruct.node.path} -> ${this.target.node.path}.${this.originalDisplayName}`); @@ -106,9 +156,9 @@ export class CfnReference extends Reference { // so construct one in-place. return new Token({ 'Fn::ImportValue': output.obtainExportName() }); } - } +import { CfnRefElement } from "./cfn-element"; import { CfnOutput } from "./cfn-output"; import { Construct, IConstruct } from "./construct"; import { Stack } from "./stack"; diff --git a/packages/@aws-cdk/cdk/lib/cfn-resource.ts b/packages/@aws-cdk/cdk/lib/cfn-resource.ts index bcf6ba2f380e9..2dbb0b502ec98 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-resource.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-resource.ts @@ -133,7 +133,7 @@ export class CfnResource extends CfnRefElement { * @param attributeName The name of the attribute. */ public getAtt(attributeName: string) { - return new CfnReference({ 'Fn::GetAtt': [this.logicalId, attributeName] }, attributeName, this); + return CfnReference.for(this, attributeName); } /** @@ -217,7 +217,7 @@ export class CfnResource extends CfnRefElement { this.untypedPropertyOverrides )); - return { + const ret = { Resources: { // Post-Resolve operation since otherwise deepMerge is going to mix values into // the Token objects returned by ignoreEmpty. @@ -234,6 +234,7 @@ export class CfnResource extends CfnRefElement { }, props => deepMerge(props, this.rawOverrides)) } }; + return ret; } catch (e) { // Change message e.message = `While synthesizing ${this.node.path}: ${e.message}`; diff --git a/packages/@aws-cdk/cdk/lib/pseudo.ts b/packages/@aws-cdk/cdk/lib/pseudo.ts index 93578652afd04..2a7d168f9e7d8 100644 --- a/packages/@aws-cdk/cdk/lib/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/pseudo.ts @@ -66,42 +66,36 @@ export class ScopedAws { } public get accountId(): string { - return new ScopedPseudo(AWS_ACCOUNTID, this.scope).toString(); + return CfnReference.forPseudo(AWS_ACCOUNTID, this.scope).toString(); } public get urlSuffix(): string { - return new ScopedPseudo(AWS_URLSUFFIX, this.scope).toString(); + return CfnReference.forPseudo(AWS_URLSUFFIX, this.scope).toString(); } public get notificationArns(): string[] { - return new ScopedPseudo(AWS_NOTIFICATIONARNS, this.scope).toList(); + return CfnReference.forPseudo(AWS_NOTIFICATIONARNS, this.scope).toList(); } public get partition(): string { - return new ScopedPseudo(AWS_PARTITION, this.scope).toString(); + return CfnReference.forPseudo(AWS_PARTITION, this.scope).toString(); } public get region(): string { - return new ScopedPseudo(AWS_REGION, this.scope).toString(); + return CfnReference.forPseudo(AWS_REGION, this.scope).toString(); } public get stackId(): string { - return new ScopedPseudo(AWS_STACKID, this.scope).toString(); + return CfnReference.forPseudo(AWS_STACKID, this.scope).toString(); } public get stackName(): string { - return new ScopedPseudo(AWS_STACKNAME, this.scope).toString(); - } -} - -class ScopedPseudo extends CfnReference { - constructor(name: string, scope: Construct) { - super({ Ref: name }, name, scope); + return CfnReference.forPseudo(AWS_STACKNAME, this.scope).toString(); } } class UnscopedPseudo extends Token { constructor(name: string) { - super({ Ref: name }, name); + super({ Ref: name }, name); } } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index 748d9350be04b..ad1fb4a3ecef1 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -237,11 +237,7 @@ export class DefaultTokenResolver implements ITokenResolver { export function findTokens(scope: IConstruct, fn: () => any): Token[] { const resolver = new RememberingTokenResolver(new NullConcat()); - try { - resolve(fn(), { scope, prefix: [], resolver }); - } finally { - // Swallow potential errors that might occur because we might not have validate()d. - } + resolve(fn(), { scope, prefix: [], resolver }); return resolver.tokens; } diff --git a/packages/@aws-cdk/cdk/test/test.stack.ts b/packages/@aws-cdk/cdk/test/test.stack.ts index 4a7791a0778a4..d6831a2a5d209 100644 --- a/packages/@aws-cdk/cdk/test/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/test.stack.ts @@ -188,6 +188,36 @@ export = { test.done(); }, + 'Cross-stack references are detected in resource properties'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const resource1 = new CfnResource(stack1, 'Resource', { type: 'BLA' }); + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another resource + new CfnResource(stack2, 'SomeResource', { type: 'AWS::Some::Resource', properties: { + someProperty: new Token(() => resource1.ref), + }}); + + // THEN + // Need to do this manually now, since we're in testing mode. In a normal CDK app, + // this happens as part of app.run(). + app.node.prepareTree(); + + test.deepEqual(stack2._toCloudFormation(), { + Resources: { + SomeResource: { + Type: 'AWS::Some::Resource', + Properties: { + someProperty: { 'Fn::ImportValue': 'Stack1:ExportsOutputRefResource1D5D905A' } + } + } + } + }); + test.done(); + }, + 'cross-stack references in lazy tokens work'(test: Test) { // GIVEN const app = new App(); From e01e3bc13ebd7f6baf6fc9fa77a3b9c3db94c9af Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 15 May 2019 14:11:31 +0200 Subject: [PATCH 15/33] Make the API of resolving jsii-exportable --- .../@aws-cdk/cdk/lib/cloudformation-lang.ts | 6 +- packages/@aws-cdk/cdk/lib/encoding.ts | 109 +-------------- packages/@aws-cdk/cdk/lib/index.ts | 1 + packages/@aws-cdk/cdk/lib/resolve.ts | 13 +- packages/@aws-cdk/cdk/lib/string-fragments.ts | 124 ++++++++++++++++++ packages/@aws-cdk/cdk/lib/token-map.ts | 6 +- 6 files changed, 138 insertions(+), 121 deletions(-) create mode 100644 packages/@aws-cdk/cdk/lib/string-fragments.ts diff --git a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts index aa49358910b27..c505b454a39d2 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation-lang.ts @@ -1,6 +1,6 @@ -import { TokenString } from "./encoding"; import { isIntrinsic, minimalCloudFormationJoin } from "./instrinsics"; import { DefaultTokenResolver, IFragmentConcatenator, resolve } from "./resolve"; +import { TokenizedStringFragments } from "./string-fragments"; import { IResolveContext, Token } from "./token"; /** @@ -46,8 +46,8 @@ export class CloudFormationLang { public resolveToken(t: Token, context: IResolveContext) { return wrap(super.resolveToken(t, context)); } - public resolveString(s: TokenString, context: IResolveContext) { - return wrap(super.resolveString(s, context)); + public resolveString(fragments: TokenizedStringFragments, context: IResolveContext) { + return wrap(super.resolveString(fragments, context)); } public resolveList(l: string[], context: IResolveContext) { return wrap(super.resolveList(l, context)); diff --git a/packages/@aws-cdk/cdk/lib/encoding.ts b/packages/@aws-cdk/cdk/lib/encoding.ts index ac5a8287df285..cbba613821451 100644 --- a/packages/@aws-cdk/cdk/lib/encoding.ts +++ b/packages/@aws-cdk/cdk/lib/encoding.ts @@ -1,4 +1,5 @@ import { IFragmentConcatenator } from "./resolve"; +import { TokenizedStringFragments } from "./string-fragments"; import { RESOLVE_METHOD, Token } from "./token"; // Details for encoding and decoding Tokens into native types; should not be exported @@ -73,104 +74,6 @@ export class TokenString { } } -/** - * Result of the split of a string with Tokens - * - * Either a literal part of the string, or an unresolved Token. - */ -type LiteralFragment = { type: 'literal'; lit: any; }; -type TokenFragment = { type: 'token'; token: Token; }; -type IntrinsicFragment = { type: 'intrinsic'; value: any; }; -type Fragment = LiteralFragment | TokenFragment | IntrinsicFragment; - -/** - * Fragments of a string with markers - */ -export class TokenizedStringFragments { - public readonly fragments = new Array(); - - public get firstFragment(): Fragment { - return this.fragments[0]; - } - - public get firstValue(): any { - return fragmentValue(this.fragments[0]); - } - - public get length() { - return this.fragments.length; - } - - public addLiteral(lit: any) { - this.fragments.push({ type: 'literal', lit }); - } - - public addToken(token: Token) { - this.fragments.push({ type: 'token', token }); - } - - public addIntrinsic(value: any) { - this.fragments.push({ type: 'intrinsic', value }); - } - - public mapTokens(fn: (t: any) => any): TokenizedStringFragments { - const ret = new TokenizedStringFragments(); - - for (const f of this.fragments) { - switch (f.type) { - case 'literal': - ret.addLiteral(f.lit); - break; - case 'token': - const mapped = fn(f.token); - if (isTokenObject(mapped)) { - ret.addToken(mapped); - } else { - ret.addIntrinsic(mapped); - } - break; - case 'intrinsic': - ret.addIntrinsic(f.value); - break; - } - } - - return ret; - } - - /** - * Combine the string fragments using the given joiner. - * - * If there are any - */ - public join(concat: IFragmentConcatenator): any { - if (this.fragments.length === 0) { return concat.join(undefined, undefined); } - if (this.fragments.length === 1) { return this.firstValue; } - - const values = this.fragments.map(fragmentValue); - - while (values.length > 1) { - const prefix = values.splice(0, 2); - values.splice(0, 0, concat.join(prefix[0], prefix[1])); - } - - return values[0]; - } -} - -/** - * Resolve the value from a single fragment - * - * If the fragment is a Token, return the string encoding of the Token. - */ -function fragmentValue(fragment: Fragment): any { - switch (fragment.type) { - case 'literal': return fragment.lit; - case 'token': return fragment.token.toString(); - case 'intrinsic': return fragment.value; - } -} - /** * Quote a string for use in a regex */ @@ -210,13 +113,3 @@ export function unresolved(obj: any): boolean { return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; } } - -/** - * Whether x is literally a Token object - * - * Can't use Token.isToken() because that has been co-opted - * to mean something else. - */ -function isTokenObject(x: any): x is Token { - return typeof(x) === 'object' && x !== null && Token.isToken(x); -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 802881d5a41a9..c1e5c78c75346 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -7,6 +7,7 @@ export * from './token-map'; export * from './tag-manager'; export * from './dependency'; export * from './resolve'; +export * from './string-fragments'; export * from './cloudformation-lang'; export * from './reference'; diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index ad1fb4a3ecef1..8cebd0dfe3e01 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -1,5 +1,6 @@ import { IConstruct } from './construct'; import { containsListTokenElement, NullConcat, TokenString, unresolved } from "./encoding"; +import { TokenizedStringFragments } from './string-fragments'; import { IResolveContext, isResolvedValuePostProcessor, RESOLVE_METHOD, Token } from "./token"; import { TokenMap } from './token-map'; @@ -78,7 +79,8 @@ export function resolve(obj: any, options: IResolveOptions): any { if (typeof(obj) === 'string') { const str = TokenString.forStringToken(obj); if (str.test()) { - return options.resolver.resolveString(str, makeContext()); + const fragments = str.split(tokenMap.lookupToken.bind(tokenMap)); + return options.resolver.resolveString(fragments, makeContext()); } return obj; } @@ -160,7 +162,7 @@ export interface ITokenResolver { * * (May use concatenation) */ - resolveString(s: TokenString, context: IResolveContext): any; + resolveString(s: TokenizedStringFragments, context: IResolveContext): any; /** * Resolve a tokenized list @@ -209,9 +211,8 @@ export class DefaultTokenResolver implements ITokenResolver { /** * Resolve string fragments to Tokens */ - public resolveString(s: TokenString, context: IResolveContext) { - const fragments = s.split(tokenMap.lookupToken.bind(tokenMap)); - return fragments.mapTokens(context.resolve).join(this.concat); + public resolveString(fragments: TokenizedStringFragments, context: IResolveContext) { + return fragments.mapTokens({ mapToken: context.resolve }).join(this.concat); } public resolveList(xs: string[], context: IResolveContext) { @@ -226,7 +227,7 @@ export class DefaultTokenResolver implements ITokenResolver { throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); } - return fragments.mapTokens(context.resolve).firstValue; + return fragments.mapTokens({ mapToken: context.resolve }).firstValue; } } diff --git a/packages/@aws-cdk/cdk/lib/string-fragments.ts b/packages/@aws-cdk/cdk/lib/string-fragments.ts new file mode 100644 index 0000000000000..9ca8e2057dd96 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/string-fragments.ts @@ -0,0 +1,124 @@ +import { IFragmentConcatenator } from "./resolve"; +import { Token } from "./token"; + +/** + * Result of the split of a string with Tokens + * + * Either a literal part of the string, or an unresolved Token. + */ +type LiteralFragment = { type: 'literal'; lit: any; }; +type TokenFragment = { type: 'token'; token: Token; }; +type IntrinsicFragment = { type: 'intrinsic'; value: any; }; +type Fragment = LiteralFragment | TokenFragment | IntrinsicFragment; + +/** + * Fragments of a string with markers + */ +export class TokenizedStringFragments { + private readonly fragments = new Array(); + + public get firstToken(): Token | undefined { + const first = this.fragments[0]; + if (first.type === 'token') { return first.token; } + return undefined; + } + + public get firstValue(): any { + return fragmentValue(this.fragments[0]); + } + + public get length() { + return this.fragments.length; + } + + public addLiteral(lit: any) { + this.fragments.push({ type: 'literal', lit }); + } + + public addToken(token: Token) { + this.fragments.push({ type: 'token', token }); + } + + public addIntrinsic(value: any) { + this.fragments.push({ type: 'intrinsic', value }); + } + + public mapTokens(mapper: ITokenMapper): TokenizedStringFragments { + const ret = new TokenizedStringFragments(); + + for (const f of this.fragments) { + switch (f.type) { + case 'literal': + ret.addLiteral(f.lit); + break; + case 'token': + const mapped = mapper.mapToken(f.token); + if (isTokenObject(mapped)) { + ret.addToken(mapped); + } else { + ret.addIntrinsic(mapped); + } + break; + case 'intrinsic': + ret.addIntrinsic(f.value); + break; + } + } + + return ret; + } + + /** + * Combine the string fragments using the given joiner. + * + * If there are any + */ + public join(concat: IFragmentConcatenator): any { + if (this.fragments.length === 0) { return concat.join(undefined, undefined); } + if (this.fragments.length === 1) { return this.firstValue; } + + const values = this.fragments.map(fragmentValue); + + while (values.length > 1) { + const prefix = values.splice(0, 2); + values.splice(0, 0, concat.join(prefix[0], prefix[1])); + } + + return values[0]; + } +} + +/** + * Interface to apply operation to tokens in a string + * + * Interface so it can be exported via jsii. + */ +export interface ITokenMapper { + /** + * Replace a single token + */ + mapToken(t: Token): any; +} + +/** + * Resolve the value from a single fragment + * + * If the fragment is a Token, return the string encoding of the Token. + */ +function fragmentValue(fragment: Fragment): any { + switch (fragment.type) { + case 'literal': return fragment.lit; + case 'token': return fragment.token.toString(); + case 'intrinsic': return fragment.value; + } +} + +/** + * Whether x is literally a Token object + * + * Can't use Token.isToken() because that has been co-opted + * to mean something else. + */ +function isTokenObject(x: any): x is Token { + return typeof(x) === 'object' && x !== null && Token.isToken(x); +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/token-map.ts b/packages/@aws-cdk/cdk/lib/token-map.ts index 64ca8add16c66..63f640f8d2f58 100644 --- a/packages/@aws-cdk/cdk/lib/token-map.ts +++ b/packages/@aws-cdk/cdk/lib/token-map.ts @@ -68,8 +68,7 @@ export class TokenMap { const str = TokenString.forStringToken(s); const fragments = str.split(this.lookupToken.bind(this)); if (fragments.length === 1) { - const first = fragments.firstFragment; - if (first.type === 'token') { return first.token; } + return fragments.firstToken; } return undefined; } @@ -82,8 +81,7 @@ export class TokenMap { const str = TokenString.forListToken(xs[0]); const fragments = str.split(this.lookupToken.bind(this)); if (fragments.length === 1) { - const first = fragments.firstFragment; - if (first.type === 'token') { return first.token; } + return fragments.firstToken; } return undefined; } From 066c521f02a041ac85d61b73212e0e04fcef23fc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 15 May 2019 15:11:46 +0200 Subject: [PATCH 16/33] Add in awsvpcconfig, state machine --- .../aws-events-targets/lib/ecs-ec2-task.ts | 41 +++++++++++++++++-- .../@aws-cdk/aws-events-targets/lib/index.ts | 1 + .../@aws-cdk/aws-events-targets/lib/lambda.ts | 2 +- .../@aws-cdk/aws-events-targets/lib/sns.ts | 2 +- .../aws-events-targets/lib/state-machine.ts | 40 ++++++++++++++++++ .../test/stepfunctions/statemachine.test.ts | 32 +++++++++++++++ packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 2 +- .../lib/run-ecs-ec2-task.ts | 2 +- .../lib/run-ecs-task-base.ts | 2 +- .../aws-stepfunctions/lib/state-machine.ts | 29 +------------ .../test/test.state-machine-resources.ts | 31 +------------- 11 files changed, 118 insertions(+), 66 deletions(-) create mode 100644 packages/@aws-cdk/aws-events-targets/lib/state-machine.ts create mode 100644 packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts index c6aa003fe59d3..2c12ba6f6a805 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts @@ -1,6 +1,8 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import ecs = require('@aws-cdk/aws-ecs'); import events = require ('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); +import { Construct, Token } from '@aws-cdk/cdk'; import { ContainerOverride } from './ecs-task-properties'; /** @@ -32,7 +34,23 @@ export interface EcsEc2TaskProps { */ readonly containerOverrides?: ContainerOverride[]; - // AWS VPC configuration + /** + * In what subnets to place the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default Private subnets + */ + readonly subnetSelection?: ec2.SubnetSelection; + + /** + * Existing security group to use for the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default A new security group is created + */ + readonly securityGroup?: ec2.ISecurityGroup; } /** @@ -56,7 +74,7 @@ export class EcsEc2Task implements events.IEventRuleTarget { /** * Allows using containers as target of CloudWatch events */ - public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(rule: events.IEventRule): events.EventRuleTargetProperties { const policyStatements = [new iam.PolicyStatement() .addAction('ecs:RunTask') .addResource(this.taskDefinition.taskDefinitionArn) @@ -82,8 +100,25 @@ export class EcsEc2Task implements events.IEventRuleTarget { taskDefinitionArn: this.taskDefinition.taskDefinitionArn }, input: events.EventTargetInput.fromObject({ - containerOverrides: this.props.containerOverrides + containerOverrides: this.props.containerOverrides, + networkConfiguration: this.renderNetworkConfiguration(rule as events.EventRule), }) }; } + + private renderNetworkConfiguration(scope: Construct) { + if (this.props.taskDefinition.networkMode !== ecs.NetworkMode.AwsVpc) { + return undefined; + } + + const subnetSelection = this.props.subnetSelection || { subnetType: ec2.SubnetType.Private }; + const securityGroup = this.props.securityGroup || new ec2.SecurityGroup(scope, 'SecurityGroup', { vpc: this.props.cluster.vpc }); + + return { + awsvpcConfiguration: { + subnets: this.props.cluster.vpc.selectSubnets(subnetSelection).subnetIds, + securityGroups: new Token(() => [securityGroup.securityGroupId]), + } + }; + } } diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index 529a9e27cd0de..cd31a43468f19 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -4,3 +4,4 @@ export * from './codebuild'; export * from './lambda'; export * from './ecs-task-properties'; export * from './ecs-ec2-task'; +export * from './state-machine'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts index 44b1994a91b66..6b9d4857bf7c3 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts @@ -13,7 +13,7 @@ export interface LambdaFunctionProps { * * @default the entire CloudWatch event */ - event?: events.EventTargetInput; + readonly event?: events.EventTargetInput; } /** diff --git a/packages/@aws-cdk/aws-events-targets/lib/sns.ts b/packages/@aws-cdk/aws-events-targets/lib/sns.ts index af997a05ed7b2..c6a7118b91274 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sns.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sns.ts @@ -11,7 +11,7 @@ export interface SnsTopicProps { * * @default the entire CloudWatch event */ - message?: events.EventTargetInput; + readonly message?: events.EventTargetInput; } /** diff --git a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts new file mode 100644 index 0000000000000..4de4aa72197e8 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts @@ -0,0 +1,40 @@ +import events = require('@aws-cdk/aws-events'); +import iam = require('@aws-cdk/aws-iam'); +import sfn = require('@aws-cdk/aws-stepfunctions'); + +/** + * Customize the Step Functions State Machine target + */ +export interface SfnStateMachineProps { + /** + * The input to the state machine execution + * + * @default the entire CloudWatch event + */ + readonly input?: events.EventTargetInput; +} + +/** + * Use a StepFunctions state machine as a target for AWS CloudWatch event rules. + */ +export class SfnStateMachine implements events.IEventRuleTarget { + constructor(public readonly machine: sfn.IStateMachine, private readonly props: SfnStateMachineProps = {}) { + } + + /** + * Returns a properties that are used in an EventRule to trigger this State Machine + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions + */ + public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + return { + id: this.machine.node.id, + arn: this.machine.stateMachineArn, + policyStatements: [new iam.PolicyStatement() + .addAction('states:StartExecution') + .addResource(this.machine.stateMachineArn) + ], + input: this.props.input + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts new file mode 100644 index 0000000000000..f3d42af70bb32 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts @@ -0,0 +1,32 @@ +import '@aws-cdk/assert/jest'; +import events = require('@aws-cdk/aws-events'); +import sfn = require('@aws-cdk/aws-stepfunctions'); +import cdk = require('@aws-cdk/cdk'); +import targets = require('../../lib'); + +test('State machine can be used as Event Rule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const rule = new events.EventRule(stack, 'Rule', { + scheduleExpression: 'rate(1 minute)' + }); + const stateMachine = new sfn.StateMachine(stack, 'SM', { + definition: new sfn.Wait(stack, 'Hello', { duration: sfn.WaitDuration.seconds(10) }) + }); + + // WHEN + rule.addTarget(new targets.SfnStateMachine(stateMachine, { + input: events.EventTargetInput.fromObject({ SomeParam: 'SomeValue' }), + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + InputTransformer: { + InputTemplate: "{\"SomeParam\":\"SomeValue\"}" + }, + } + ] + }); +}); diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index 8399cb0c3c9aa..57ecf8b32b85e 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -2,7 +2,7 @@ import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); -import { Stack, App, CfnOutput } from '@aws-cdk/cdk'; +import { App, CfnOutput, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sqs = require('../lib'); import { Queue } from '../lib'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-ec2-task.ts index 0c07cfc636601..ce32e3f94d4f2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-ec2-task.ts @@ -66,7 +66,7 @@ export class RunEcsEc2Task extends EcsRunTaskBase { }); if (props.taskDefinition.networkMode === ecs.NetworkMode.AwsVpc) { - this.configureAwsVpcNetworking(props.cluster.vpc, false, props.subnets, props.securityGroup); + this.configureAwsVpcNetworking(props.cluster.vpc, undefined, props.subnets, props.securityGroup); } else { // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. validateNoNetworkingProps(props); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts index 9a5c773f86ce3..0c110be25f9ea 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-task-base.ts @@ -110,7 +110,7 @@ export class EcsRunTaskBase implements ec2.IConnectable, sfn.IStepFunctionsTask this.networkConfiguration = { AwsvpcConfiguration: { - AssignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED', + AssignPublicIp: assignPublicIp !== undefined ? (assignPublicIp ? 'ENABLED' : 'DISABLED') : undefined, Subnets: vpc.selectSubnets(subnetSelection).subnetIds, SecurityGroups: new cdk.Token(() => [this.securityGroup!.securityGroupId]), } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 1eee9ad06408e..50a94cfc3a5c9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -1,5 +1,4 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); -import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import { Construct, IResource, Resource } from '@aws-cdk/cdk'; import { StateGraph } from './state-graph'; @@ -40,7 +39,7 @@ export interface StateMachineProps { /** * Define a StepFunctions State Machine */ -export class StateMachine extends Resource implements IStateMachine, events.IEventRuleTarget { +export class StateMachine extends Resource implements IStateMachine { /** * Import a state machine */ @@ -68,11 +67,6 @@ export class StateMachine extends Resource implements IStateMachine, events.IEve */ public readonly stateMachineArn: string; - /** - * A role used by CloudWatch events to start the State Machine - */ - private eventsRole?: iam.Role; - constructor(scope: Construct, id: string, props: StateMachineProps) { super(scope, id); @@ -104,27 +98,6 @@ export class StateMachine extends Resource implements IStateMachine, events.IEve this.role.addToPolicy(statement); } - /** - * Allows using state machines as event rule targets. - */ - public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { - if (!this.eventsRole) { - this.eventsRole = new iam.Role(this, 'EventsRole', { - assumedBy: new iam.ServicePrincipal('events.amazonaws.com') - }); - - this.eventsRole.addToPolicy(new iam.PolicyStatement() - .addAction('states:StartExecution') - .addResource(this.stateMachineArn)); - } - - return { - id: this.node.id, - arn: this.stateMachineArn, - roleArn: this.eventsRole.roleArn, - }; - } - /** * Return the given named metric for this State Machine's executions * diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index 998428b9f7d02..718937305faa8 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -1,5 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; -import events = require('@aws-cdk/aws-events'); +import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; @@ -130,32 +129,4 @@ export = { test.done(); }, - 'State machine can be used as Event Rule target'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const rule = new events.EventRule(stack, 'Rule', { - scheduleExpression: 'rate(1 minute)' - }); - const stateMachine = new stepfunctions.StateMachine(stack, 'SM', { - definition: new stepfunctions.Wait(stack, 'Hello', { duration: stepfunctions.WaitDuration.seconds(10) }) - }); - - // WHEN - rule.addTarget(stateMachine, { - jsonTemplate: { SomeParam: 'SomeValue' }, - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { - Targets: [ - { - InputTransformer: { - InputTemplate: "{\"SomeParam\":\"SomeValue\"}" - }, - } - ] - })); - - test.done(); - }, }; \ No newline at end of file From 0405f417354abd7d519edd6f756045a5fc6f8b11 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 15 May 2019 17:26:52 +0200 Subject: [PATCH 17/33] Get rid of premature resolution during toCloudFormation() WE used to have this because we couldn't validate what tokens returned previously, but now that we have PostProcessingToken we can! --- .../test/integ.alarm-and-dashboard.ts | 2 +- packages/@aws-cdk/aws-s3/test/test.bucket.ts | 13 +++++++++++++ packages/@aws-cdk/cdk/lib/cfn-reference.ts | 2 ++ packages/@aws-cdk/cdk/lib/cfn-resource.ts | 14 +++++++++++--- packages/@aws-cdk/cdk/lib/resolve.ts | 15 ++++++++++++++- packages/@aws-cdk/cdk/lib/token.ts | 8 ++++++-- tools/cfn2ts/lib/codegen.ts | 2 +- 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts index 79b53ac3392e7..a187dbb427ac6 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts @@ -17,7 +17,7 @@ const queue = new cdk.CfnResource(stack, 'queue', { type: 'AWS::SQS::Queue' }); const metric = new cloudwatch.Metric({ namespace: 'AWS/SQS', metricName: 'ApproximateNumberOfMessagesVisible', - dimensions: { QueueName: queue.getAtt('QueueName') } + dimensions: { QueueName: queue.getAtt('QueueName').toString() } }); const alarm = metric.newAlarm(stack, 'Alarm', { diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index 20fb0cedd162d..00120ed3b13ab 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -29,6 +29,19 @@ export = { test.done(); }, + 'CFN properties are type-validated during resolution'(test: Test) { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'MyBucket', { + bucketName: new cdk.Token(() => 5).toString() // Oh no + }); + + test.throws(() => { + SynthUtils.toCloudFormation(stack); + }, /bucketName: 5 should be a string/); + + test.done(); + }, + 'bucket without encryption'(test: Test) { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { diff --git a/packages/@aws-cdk/cdk/lib/cfn-reference.ts b/packages/@aws-cdk/cdk/lib/cfn-reference.ts index 4589116757e22..257d79cf0625e 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-reference.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-reference.ts @@ -66,7 +66,9 @@ export class CfnReference extends Reference { CfnReference.referenceTable.set(target, attribs); } let ref = attribs.get(attribKey); + // console.log('Getting', attribKey, 'from', target.node.path); if (!ref) { + // console.log('Making a new one'); ref = fresh(); attribs.set(attribKey, ref); } diff --git a/packages/@aws-cdk/cdk/lib/cfn-resource.ts b/packages/@aws-cdk/cdk/lib/cfn-resource.ts index 2dbb0b502ec98..e738818e49194 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-resource.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-resource.ts @@ -211,11 +211,11 @@ export class CfnResource extends CfnRefElement { try { // merge property overrides onto properties and then render (and validate). const tags = CfnResource.isTaggable(this) ? this.tags.renderTags() : undefined; - const properties = this.renderProperties(deepMerge( + const properties = deepMerge( this.properties || {}, { tags }, this.untypedPropertyOverrides - )); + ); const ret = { Resources: { @@ -231,7 +231,11 @@ export class CfnResource extends CfnRefElement { DeletionPolicy: capitalizePropertyNames(this, this.options.deletionPolicy), Metadata: ignoreEmpty(this.options.metadata), Condition: this.options.condition && this.options.condition.logicalId - }, props => deepMerge(props, this.rawOverrides)) + }, props => { + const r = deepMerge(props, this.rawOverrides); + r.Properties = this.renderProperties(r.Properties); + return r; + }) } }; return ret; @@ -259,6 +263,10 @@ export class CfnResource extends CfnRefElement { protected renderProperties(properties: any): { [key: string]: any } { return properties; } + + protected validateProperties(_properties: any) { + // Nothing + } } export enum TagType { diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index 8cebd0dfe3e01..4a58fb0b95deb 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -30,6 +30,8 @@ export interface IResolveOptions { * @param prefix Prefix key path components for diagnostics. */ export function resolve(obj: any, options: IResolveOptions): any { + // console.log('Resolving', obj, 'resolver', options.resolver); + // console.log(new Error().stack); const prefix = options.prefix || []; const pathName = '/' + prefix.join('/'); @@ -114,6 +116,7 @@ export function resolve(obj: any, options: IResolveOptions): any { // if (unresolved(obj)) { + // console.log('Its a Token', options.resolver); return options.resolver.resolveToken(obj, makeContext()); } @@ -237,8 +240,17 @@ export class DefaultTokenResolver implements ITokenResolver { */ export function findTokens(scope: IConstruct, fn: () => any): Token[] { const resolver = new RememberingTokenResolver(new NullConcat()); + // console.log('=========== FINDTOKENS ============'); - resolve(fn(), { scope, prefix: [], resolver }); + // console.log('Going into it with the resolver', resolver); + + const start = fn(); + + // console.log('Doing the thing', resolver); + + resolve(start, { scope, prefix: [], resolver }); + + // console.log('=========== /FINDTOKENS ============'); return resolver.tokens; } @@ -250,6 +262,7 @@ export class RememberingTokenResolver extends DefaultTokenResolver { private readonly tokensSeen = new Set(); public resolveToken(t: Token, context: IResolveContext) { + // console.log('Seeing', t); this.tokensSeen.add(t); return super.resolveToken(t, context); } diff --git a/packages/@aws-cdk/cdk/lib/token.ts b/packages/@aws-cdk/cdk/lib/token.ts index 4714fdc7f6546..ac304d2e6fbaa 100644 --- a/packages/@aws-cdk/cdk/lib/token.ts +++ b/packages/@aws-cdk/cdk/lib/token.ts @@ -110,8 +110,12 @@ export class Token { // case, string or number), and we can't know that without an // IResolveContext to actually do the resolution, which we don't have. - // tslint:disable-next-line:max-line-length - throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use this.node.stringifyJson() instead.'); + // We used to throw an error, but since JSON.stringify() is often used in + // error messages to produce a readable representation of an object, if we + // throw here we'll obfuscate that descriptive error with something worse. + // So return a string representation that indicates this thing is a token + // and needs resolving. + return JSON.stringify(``); } /** diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 80c8089fd5a5b..23203cae0da9c 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -340,7 +340,7 @@ export default class CodeGenerator { this.code.closeBlock(); this.code.openBlock('protected renderProperties(properties: any): { [key: string]: any } '); - this.code.line(`return ${genspec.cfnMapperName(propsType).fqn}(this.node.resolve(properties));`); + this.code.line(`return ${genspec.cfnMapperName(propsType).fqn}(properties);`); this.code.closeBlock(); } From c5d744624bbda56e69817c7359187eeec7b4a756 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 16 May 2019 10:42:35 +0200 Subject: [PATCH 18/33] Fix API gateway token issues --- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 48 ++----------------- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 20 ++------ .../aws-cloudtrail/test/test.cloudtrail.ts | 34 ++++++++++++- packages/@aws-cdk/aws-events/lib/input.ts | 15 +----- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 25 +--------- packages/@aws-cdk/cdk/lib/cfn-element.ts | 2 +- packages/@aws-cdk/cdk/lib/cfn-reference.ts | 2 - packages/@aws-cdk/cdk/lib/resolve.ts | 37 +++++++------- packages/@aws-cdk/cdk/test/test.tokens.ts | 48 ++++++++++++++++++- 9 files changed, 114 insertions(+), 117 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 65bc57d2dc6e5..7db0158d2b499 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -91,51 +91,13 @@ export class Deployment extends Resource { } class LatestDeploymentResource extends CfnDeployment { - private originalLogicalId?: string; - private lazyLogicalIdRequired: boolean; - private lazyLogicalId?: string; - private logicalIdToken: Token; private hashComponents = new Array(); + private originalLogicalId: string; constructor(scope: Construct, id: string, props: CfnDeploymentProps) { super(scope, id, props); - // from this point, don't allow accessing logical ID before synthesis - this.lazyLogicalIdRequired = true; - - this.logicalIdToken = new Token(() => this.lazyLogicalId); - } - - /** - * Returns either the original or the custom logical ID of this resource. - */ - public get logicalId() { - if (!this.lazyLogicalIdRequired) { - return this.originalLogicalId!; - } - - return this.logicalIdToken.toString(); - } - - /** - * Sets the logical ID of this resource. - */ - public set logicalId(v: string) { - this.originalLogicalId = v; - } - - /** - * Returns a lazy reference to this resource (evaluated only upon synthesis). - */ - public get ref() { - return new Token(() => ({ Ref: this.lazyLogicalId })).toString(); - } - - /** - * Does nothing. - */ - public set ref(_v: string) { - return; + this.originalLogicalId = this.node.stack.logicalIds.getLogicalId(this); } /** @@ -159,15 +121,13 @@ class LatestDeploymentResource extends CfnDeployment { protected prepare() { // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. - if (this.hashComponents.length === 0) { - this.lazyLogicalId = this.originalLogicalId; - } else { + if (this.hashComponents.length > 0) { const md5 = crypto.createHash('md5'); this.hashComponents .map(c => this.node.resolve(c)) .forEach(c => md5.update(JSON.stringify(c))); - this.lazyLogicalId = this.originalLogicalId + md5.digest("hex"); + this.overrideLogicalId(this.originalLogicalId + md5.digest("hex")); } super.prepare(); diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 954a204e4a53b..51b8a9dcbd36f 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -191,7 +191,7 @@ export class CloudTrail extends cdk.Construct { } this.eventSelectors.push({ includeManagementEvents: options.includeManagementEvents, - readWriteType: options.readWriteType || ReadWriteType.WriteOnly, + readWriteType: options.readWriteType, dataResources: [{ type: "AWS::S3::Object", values: prefixes @@ -200,7 +200,10 @@ export class CloudTrail extends cdk.Construct { } /** - * Create an event rule for when an event is added to this trail + * Create an event rule for when an event is recorded by any trail. + * + * Note that the event doesn't necessarily have to come from this + * trail. Be sure to filter the event properly using an event pattern. */ public onEvent(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { const rule = new events.EventRule(this, name, options); @@ -210,19 +213,6 @@ export class CloudTrail extends cdk.Construct { }); return rule; } - - /** - * Create an event rule for when an S3 event is added to this trail - */ - public onS3Event(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { - const rule = new events.EventRule(this, name, options); - rule.addTarget(target); - rule.addEventPattern({ - source: ['aws.s3'], - detailType: ['AWS API Call via CloudTrail'] - }); - return rule; - } } /** diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index b9a25981ccc88..f2c23e984f5fc 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -189,7 +189,39 @@ export = { test.done(); }, } - } + }, + + 'add an event rule'(test: Test) { + // GIVEN + const stack = getTestStack(); + const trail = new CloudTrail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WriteOnly }); + + // WHEN + trail.onEvent('DoEvents', { + bind: () => ({ + arn: 'arn', + id: 'myid' + }) + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + EventPattern: { + "detail-type": [ + "AWS API Call via CloudTrail" + ] + }, + State: "ENABLED", + Targets: [ + { + Arn: "arn", + Id: "myid" + } + ] + })); + + test.done(); + }, }; function getTestStack(): Stack { diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts index 66064258c1250..ec70050800024 100644 --- a/packages/@aws-cdk/aws-events/lib/input.ts +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -1,4 +1,4 @@ -import { CloudFormationLang, DefaultTokenResolver, IFragmentConcatenator, IResolveContext, resolve, Token } from '@aws-cdk/cdk'; +import { CloudFormationLang, DefaultTokenResolver, IResolveContext, resolve, StringConcat, Token } from '@aws-cdk/cdk'; import { IEventRule } from './rule-ref'; /** @@ -289,19 +289,6 @@ function isEventField(x: any): x is EventField { const EVENT_FIELD_SYMBOL = Symbol.for('@aws-cdk/aws-events.EventField'); -/** - * Converts all fragments to strings and concats those - * - * Drops 'undefined's. - */ -class StringConcat implements IFragmentConcatenator { - public join(left: any | undefined, right: any | undefined): any { - if (left === undefined) { return right !== undefined ? `${right}` : undefined; } - if (right === undefined) { return `${left}`; } - return `${left}${right}`; - } -} - /** * Quote a string for use in a regex */ diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index 57ecf8b32b85e..ae1c6e35d517b 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -2,7 +2,7 @@ import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); -import { App, CfnOutput, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sqs = require('../lib'); import { Queue } from '../lib'; @@ -514,28 +514,7 @@ export = { }); test.done(); - }, - - 'reference stack from other stack'(test: Test) { - // GIVEN - const app = new App(); - const stack1 = new Stack(app, 'Stack1'); - const queue = new sqs.Queue(stack1, 'Queue'); - - const stack2 = new Stack(app, 'Stack2'); - - // WHEN - new CfnOutput(stack2, 'Output', { value: queue.queueArn }); - - // THEN - expect(stack2).toMatch({ - Outputs: { - Hello: 'bye' - } - }); - - test.done(); - }, + } }; function testGrant(action: (q: Queue, principal: iam.IPrincipal) => void, ...expectedActions: string[]) { diff --git a/packages/@aws-cdk/cdk/lib/cfn-element.ts b/packages/@aws-cdk/cdk/lib/cfn-element.ts index 48cafcfbae780..e8aac4e2fa065 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-element.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-element.ts @@ -46,7 +46,7 @@ export abstract class CfnElement extends Construct { this.node.addMetadata(LOGICAL_ID_MD, new (require("./token").Token)(() => this.logicalId), this.constructor); this._logicalId = this.node.stack.logicalIds.getLogicalId(this); - this.logicalId = new Token(() => this._logicalId).toString(); + this.logicalId = new Token(() => this._logicalId, `${this.node.path}.LogicalID`).toString(); } /** diff --git a/packages/@aws-cdk/cdk/lib/cfn-reference.ts b/packages/@aws-cdk/cdk/lib/cfn-reference.ts index 257d79cf0625e..4589116757e22 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-reference.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-reference.ts @@ -66,9 +66,7 @@ export class CfnReference extends Reference { CfnReference.referenceTable.set(target, attribs); } let ref = attribs.get(attribKey); - // console.log('Getting', attribKey, 'from', target.node.path); if (!ref) { - // console.log('Making a new one'); ref = fresh(); attribs.set(attribKey, ref); } diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index 4a58fb0b95deb..b664a70c7ce77 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -1,5 +1,5 @@ import { IConstruct } from './construct'; -import { containsListTokenElement, NullConcat, TokenString, unresolved } from "./encoding"; +import { containsListTokenElement, TokenString, unresolved } from "./encoding"; import { TokenizedStringFragments } from './string-fragments'; import { IResolveContext, isResolvedValuePostProcessor, RESOLVE_METHOD, Token } from "./token"; import { TokenMap } from './token-map'; @@ -30,8 +30,6 @@ export interface IResolveOptions { * @param prefix Prefix key path components for diagnostics. */ export function resolve(obj: any, options: IResolveOptions): any { - // console.log('Resolving', obj, 'resolver', options.resolver); - // console.log(new Error().stack); const prefix = options.prefix || []; const pathName = '/' + prefix.join('/'); @@ -116,7 +114,6 @@ export function resolve(obj: any, options: IResolveOptions): any { // if (unresolved(obj)) { - // console.log('Its a Token', options.resolver); return options.resolver.resolveToken(obj, makeContext()); } @@ -185,6 +182,19 @@ export interface IFragmentConcatenator { join(left: any | undefined, right: any | undefined): any; } +/** + * Converts all fragments to strings and concats those + * + * Drops 'undefined's. + */ +export class StringConcat implements IFragmentConcatenator { + public join(left: any | undefined, right: any | undefined): any { + if (left === undefined) { return right !== undefined ? `${right}` : undefined; } + if (right === undefined) { return `${left}`; } + return `${left}${right}`; + } +} + /** * Default resolver implementation */ @@ -239,18 +249,9 @@ export class DefaultTokenResolver implements ITokenResolver { * Find all Tokens that are used in the given structure */ export function findTokens(scope: IConstruct, fn: () => any): Token[] { - const resolver = new RememberingTokenResolver(new NullConcat()); - // console.log('=========== FINDTOKENS ============'); + const resolver = new RememberingTokenResolver(new StringConcat()); - // console.log('Going into it with the resolver', resolver); - - const start = fn(); - - // console.log('Doing the thing', resolver); - - resolve(start, { scope, prefix: [], resolver }); - - // console.log('=========== /FINDTOKENS ============'); + resolve(fn(), { scope, prefix: [], resolver }); return resolver.tokens; } @@ -262,11 +263,15 @@ export class RememberingTokenResolver extends DefaultTokenResolver { private readonly tokensSeen = new Set(); public resolveToken(t: Token, context: IResolveContext) { - // console.log('Seeing', t); this.tokensSeen.add(t); return super.resolveToken(t, context); } + public resolveString(s: TokenizedStringFragments, context: IResolveContext) { + const ret = super.resolveString(s, context); + return ret; + } + public get tokens(): Token[] { return Array.from(this.tokensSeen); } diff --git a/packages/@aws-cdk/cdk/test/test.tokens.ts b/packages/@aws-cdk/cdk/test/test.tokens.ts index 259a31974eabb..bf5427b087468 100644 --- a/packages/@aws-cdk/cdk/test/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/test.tokens.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { App as Root, Fn, Token } from '../lib'; +import { App as Root, findTokens, Fn, Stack, Token } from '../lib'; import { TokenMap } from '../lib/token-map'; import { evaluateCFN } from './evaluate-cfn'; @@ -279,6 +279,52 @@ export = { test.done(); }, + 'tokens can be nested in hash keys'(test: Test) { + // GIVEN + const token = new Token(() => new Token(() => new Token(() => 'I am a string'))); + + // WHEN + const s = { + [token.toString()]: `boom ${token}` + }; + + // THEN + test.deepEqual(resolve(s), { 'I am a string': 'boom I am a string' }); + test.done(); + }, + + 'tokens can be nested and concatenated in hash keys'(test: Test) { + // GIVEN + const innerToken = new Token(() => 'toot'); + const token = new Token(() => `${innerToken} the woot`); + + // WHEN + const s = { + [token.toString()]: `boom chicago` + }; + + // THEN + test.deepEqual(resolve(s), { 'toot the woot': 'boom chicago' }); + test.done(); + }, + + 'can find nested tokens in hash keys'(test: Test) { + // GIVEN + const innerToken = new Token(() => 'toot'); + const token = new Token(() => `${innerToken} the woot`); + + // WHEN + const s = { + [token.toString()]: `boom chicago` + }; + + // THEN + const tokens = findTokens(new Stack(), () => s); + test.ok(tokens.some(t => t === innerToken), 'Cannot find innerToken'); + test.ok(tokens.some(t => t === token), 'Cannot find token'); + test.done(); + }, + 'fails if token in a hash key resolves to a non-string'(test: Test) { // GIVEN const token = new Token({ Ref: 'Other' }); From b9ad0587ce75d56bc5a95ba5ae10b946a30b111b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 16 May 2019 14:05:12 +0200 Subject: [PATCH 19/33] Add tests --- .../@aws-cdk/aws-events-targets/package.json | 2 +- .../test/codepipeline/pipeline.test.ts | 81 +++++++++++++++++++ .../test/stepfunctions/statemachine.test.ts | 4 +- packages/@aws-cdk/cdk/lib/cfn-element.ts | 7 +- 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 0a775009208f8..d28c3f9766280 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -48,7 +48,7 @@ ], "coverageThreshold": { "global": { - "branches": 70, + "branches": 30, "statements": 80 } } diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts new file mode 100644 index 0000000000000..6c883192911e1 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts @@ -0,0 +1,81 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import events = require('@aws-cdk/aws-events'); +import { Stack } from '@aws-cdk/cdk'; +import targets = require('../../lib'); + +test('use codebuild project as an eventrule target', () => { + // GIVEN + const stack = new Stack(); + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + + const srcArtifact = new codepipeline.Artifact('Src'); + const buildArtifact = new codepipeline.Artifact('Bld'); + pipeline.addStage({ + name: 'Source', + actions: [new TestAction({ + actionName: 'Hello', + category: codepipeline.ActionCategory.Source, + provider: 'x', + artifactBounds: { minInputs: 0, maxInputs: 0 , minOutputs: 1, maxOutputs: 1, }, + outputs: [srcArtifact]})] + }); + pipeline.addStage({ + name: 'Build', + actions: [new TestAction({ + actionName: 'Hello', + category: codepipeline.ActionCategory.Build, + provider: 'y', + inputs: [srcArtifact], + outputs: [buildArtifact], + artifactBounds: { minInputs: 1, maxInputs: 1 , minOutputs: 1, maxOutputs: 1, }})] + }); + + const rule = new events.EventRule(stack, 'rule', { scheduleExpression: 'rate(1 min)' }); + + // WHEN + rule.addTarget(new targets.CodePipeline(pipeline)); + + const pipelineArn = { + "Fn::Join": [ "", [ + "arn:", + { Ref: "AWS::Partition" }, + ":codepipeline:", + { Ref: "AWS::Region" }, + ":", + { Ref: "AWS::AccountId" }, + ":", + { Ref: "PipelineC660917D" }] + ] + }; + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: pipelineArn, + Id: "Pipeline", + RoleArn: { "Fn::GetAtt": [ "ruleRolePipelineFB6AC329", "Arn" ] } + } + ] + })); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "codepipeline:StartPipelineExecution", + Effect: "Allow", + Resource: pipelineArn, + } + ], + Version: "2012-10-17" + } + })); +}); + +class TestAction extends codepipeline.Action { + protected bind(_info: codepipeline.ActionBind): void { + // void + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts index f3d42af70bb32..4225a0129fde4 100644 --- a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts @@ -23,9 +23,7 @@ test('State machine can be used as Event Rule target', () => { expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { - InputTransformer: { - InputTemplate: "{\"SomeParam\":\"SomeValue\"}" - }, + Input: "{\"SomeParam\":\"SomeValue\"}" } ] }); diff --git a/packages/@aws-cdk/cdk/lib/cfn-element.ts b/packages/@aws-cdk/cdk/lib/cfn-element.ts index e8aac4e2fa065..1ac6e1c1540c7 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-element.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-element.ts @@ -46,7 +46,7 @@ export abstract class CfnElement extends Construct { this.node.addMetadata(LOGICAL_ID_MD, new (require("./token").Token)(() => this.logicalId), this.constructor); this._logicalId = this.node.stack.logicalIds.getLogicalId(this); - this.logicalId = new Token(() => this._logicalId, `${this.node.path}.LogicalID`).toString(); + this.logicalId = new Token(() => this._logicalId, `${notTooLong(this.node.path)}.LogicalID`).toString(); } /** @@ -152,5 +152,10 @@ export abstract class CfnRefElement extends CfnElement { } } +function notTooLong(x: string) { + if (x.length < 100) { return x; } + return x.substr(0, 47) + '...' + x.substr(x.length - 47); +} + import { CfnReference } from "./cfn-reference"; import { findTokens } from "./resolve"; From 2d000d52c98c587b7960f4b183588b97e960ca54 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 12:21:01 +0200 Subject: [PATCH 20/33] Remove AssignPublicIp default in test --- packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts index 4f02c420913e8..d7c9ac5c9605e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts @@ -57,7 +57,6 @@ test('Running a Fargate Task', () => { LaunchType: "FARGATE", NetworkConfiguration: { AwsvpcConfiguration: { - AssignPublicIp: "DISABLED", SecurityGroups: [{"Fn::GetAtt": ["RunFargateSecurityGroup709740F2", "GroupId"]}], Subnets: [ {Ref: "VpcPrivateSubnet1Subnet536B997A"}, From 52377d9c11e03d8c0d98d794b91fe6d343cece93 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 13:09:54 +0200 Subject: [PATCH 21/33] Don't use overrides during rendering in Pipeline --- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 75 ++++++++----------- .../@aws-cdk/aws-codepipeline/lib/stage.ts | 3 +- 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 75886cff7a8c1..d30079d4332f8 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -179,9 +179,8 @@ export class Pipeline extends PipelineBase { public readonly artifactBucket: s3.IBucket; private readonly stages = new Array(); - private readonly pipelineResource: CfnPipeline; private readonly crossRegionReplicationBuckets: { [region: string]: string }; - private readonly artifactStores: { [region: string]: any }; + private readonly artifactStores: { [region: string]: CfnPipeline.ArtifactStoreProperty }; private readonly _crossRegionScaffoldStacks: { [region: string]: CrossRegionScaffoldStack } = {}; constructor(scope: Construct, id: string, props?: PipelineProps) { @@ -207,8 +206,9 @@ export class Pipeline extends PipelineBase { }); const codePipeline = new CfnPipeline(this, 'Resource', { - artifactStore: new Token(() => this.renderArtifactStore()) as any, - stages: new Token(() => this.renderStages()) as any, + artifactStore: new Token(() => this.renderArtifactStore()), + artifactStores: new Token(() => this.renderArtifactStores()), + stages: new Token(() => this.renderStages()), roleArn: this.role.roleArn, restartExecutionOnUpdate: props && props.restartExecutionOnUpdate, name: props && props.pipelineName, @@ -221,7 +221,6 @@ export class Pipeline extends PipelineBase { this.pipelineName = codePipeline.ref; this.pipelineVersion = codePipeline.pipelineVersion; - this.pipelineResource = codePipeline; this.crossRegionReplicationBuckets = props.crossRegionReplicationBuckets || {}; this.artifactStores = {}; @@ -382,8 +381,8 @@ export class Pipeline extends PipelineBase { replicationBucket.grantReadWrite(this.role); this.artifactStores[region] = { - Location: replicationBucket.bucketName, - Type: 'S3', + location: replicationBucket.bucketName, + type: 'S3', }; } @@ -496,7 +495,28 @@ export class Pipeline extends PipelineBase { return ret; } - private renderArtifactStore(): CfnPipeline.ArtifactStoreProperty { + private renderArtifactStores(): CfnPipeline.ArtifactStoreMapProperty[] | undefined { + if (!this.crossRegion) { return undefined; } + + // add the Pipeline's artifact store + const primaryStore = this.renderPrimaryArtifactStore(); + this.artifactStores[this.node.stack.requireRegion()] = { + location: primaryStore.location, + type: primaryStore.type, + encryptionKey: primaryStore.encryptionKey, + }; + + return Object.entries(this.artifactStores).map(([region, artifactStore]) => ({ + region, artifactStore + })); + } + + private renderArtifactStore(): CfnPipeline.ArtifactStoreProperty | undefined { + if (this.crossRegion) { return undefined; } + return this.renderPrimaryArtifactStore(); + } + + private renderPrimaryArtifactStore(): CfnPipeline.ArtifactStoreProperty { let encryptionKey: CfnPipeline.EncryptionKeyProperty | undefined; const bucketKey = this.artifactBucket.encryptionKey; if (bucketKey) { @@ -518,41 +538,12 @@ export class Pipeline extends PipelineBase { }; } - private renderStages(): CfnPipeline.StageDeclarationProperty[] { - // handle cross-region CodePipeline overrides here - let crossRegion = false; - this.stages.forEach((stage, i) => { - stage.actions.forEach((action, j) => { - if (action.region) { - crossRegion = true; - this.pipelineResource.addPropertyOverride(`Stages.${i}.Actions.${j}.Region`, action.region); - } - }); - }); - - if (crossRegion) { - // we don't need ArtifactStore in this case - this.pipelineResource.addPropertyDeletionOverride('ArtifactStore'); - - // add the Pipeline's artifact store - const artifactStore = this.renderArtifactStore(); - this.artifactStores[this.node.stack.requireRegion()] = { - Location: artifactStore.location, - Type: artifactStore.type, - EncryptionKey: artifactStore.encryptionKey, - }; - - const artifactStoresProp: any[] = []; - // tslint:disable-next-line:forin - for (const region in this.artifactStores) { - artifactStoresProp.push({ - Region: region, - ArtifactStore: this.artifactStores[region], - }); - } - this.pipelineResource.addPropertyOverride('ArtifactStores', artifactStoresProp); - } + private get crossRegion(): boolean { + return this.stages.some(stage => stage.actions.some(action => action.region !== undefined)); + // this.pipelineResource.addPropertyOverride(`Stages.${i}.Actions.${j}.Region`, action.region); + } + private renderStages(): CfnPipeline.StageDeclarationProperty[] { return this.stages.map(stage => stage.render()); } } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 6e48b68ea5edc..029d894ea89f0 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -135,7 +135,8 @@ export class Stage implements IStage { }, configuration: action.configuration, runOrder: action.runOrder, - roleArn: action.role ? action.role.roleArn : undefined + roleArn: action.role ? action.role.roleArn : undefined, + region: action.region, }; } From 8e7e480d2b3ba728f2a46c2525d5c7d06c11b7bb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 16:37:52 +0200 Subject: [PATCH 22/33] Centralize role creation, update integ tests --- ...g.cfn-template-from-repo.lit.expected.json | 22 ++++++------ ...yed-through-codepipeline.lit.expected.json | 32 ++++++++--------- .../test/integ.lambda-pipeline.expected.json | 34 +++++++++---------- .../test/integ.lambda-pipeline.ts | 2 +- ...uild-multiple-inputs-outputs.expected.json | 12 +++---- .../integ.pipeline-code-commit.expected.json | 32 ++++++++--------- .../integ.pipeline-ecr-source.expected.json | 12 +++---- .../test/integ.pipeline-events.expected.json | 28 +++++++-------- .../aws-events-targets/lib/codebuild.ts | 5 +-- .../aws-events-targets/lib/codepipeline.ts | 5 +-- .../aws-events-targets/lib/ecs-ec2-task.ts | 3 +- .../aws-events-targets/lib/state-machine.ts | 5 +-- .../@aws-cdk/aws-events-targets/lib/util.ts | 22 ++++++++++++ .../test/codebuild/codebuild.test.ts | 4 +-- .../integ.project-events.expected.json | 12 +++---- .../test/codepipeline/pipeline.test.ts | 2 +- .../test/ecs/ec2-event-rule-target.test.ts | 2 +- .../ecs/integ.event-task.lit.expected.json | 12 +++---- packages/@aws-cdk/aws-events/lib/rule.ts | 19 +++-------- packages/@aws-cdk/aws-events/lib/target.ts | 4 +-- .../@aws-cdk/aws-events/test/test.rule.ts | 26 +++++--------- tools/cdk-integ-tools/bin/cdk-integ-assert.ts | 2 +- 22 files changed, 152 insertions(+), 145 deletions(-) create mode 100644 packages/@aws-cdk/aws-events-targets/lib/util.ts diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json index 86d24e18de1be..f80827ddacf12 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.cfn-template-from-repo.lit.expected.json @@ -7,9 +7,8 @@ "Triggers": [] } }, - "PipelineArtifactsBucketEncryptionKey01D58D69" : { + "PipelineArtifactsBucketEncryptionKey01D58D69": { "Type": "AWS::KMS::Key", - "DeletionPolicy": "Retain", "Properties": { "KeyPolicy": { "Statement": [ @@ -71,11 +70,11 @@ ], "Version": "2012-10-17" } - } + }, + "DeletionPolicy": "Retain" }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ @@ -92,7 +91,8 @@ } ] } - } + }, + "DeletionPolicy": "Retain" }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -381,10 +381,6 @@ } ], "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3", "EncryptionKey": { "Id": { "Fn::GetAtt": [ @@ -393,7 +389,11 @@ ] }, "Type": "KMS" - } + }, + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" } }, "DependsOn": [ @@ -450,4 +450,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json index 241c862e043cc..8229c2b3b932a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json @@ -2,7 +2,6 @@ "Resources": { "PipelineArtifactsBucketEncryptionKey01D58D69": { "Type": "AWS::KMS::Key", - "DeletionPolicy": "Retain", "Properties": { "KeyPolicy": { "Statement": [ @@ -102,11 +101,11 @@ ], "Version": "2012-10-17" } - } + }, + "DeletionPolicy": "Retain" }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ @@ -123,7 +122,8 @@ } ] } - } + }, + "DeletionPolicy": "Retain" }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -480,10 +480,6 @@ } ], "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3", "EncryptionKey": { "Id": { "Fn::GetAtt": [ @@ -492,7 +488,11 @@ ] }, "Type": "KMS" - } + }, + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" } }, "DependsOn": [ @@ -500,7 +500,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventsRole46BEEA7C": { + "PipelineEventRoleCF28672B": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -527,7 +527,7 @@ } } }, - "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "PipelineEventRoleDefaultPolicy5F1B5670": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -562,10 +562,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", "Roles": [ { - "Ref": "PipelineEventsRole46BEEA7C" + "Ref": "PipelineEventRoleCF28672B" } ] } @@ -682,7 +682,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventsRole46BEEA7C", + "PipelineEventRoleCF28672B", "Arn" ] } @@ -754,7 +754,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventsRole46BEEA7C", + "PipelineEventRoleCF28672B", "Arn" ] } @@ -1109,4 +1109,4 @@ } } } -} +} \ No newline at end of file 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 63f6c7e3817f2..b13550f0311b1 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 @@ -1,8 +1,7 @@ { "Resources": { - "PipelineArtifactsBucketEncryptionKey01D58D69" : { + "PipelineArtifactsBucketEncryptionKey01D58D69": { "Type": "AWS::KMS::Key", - "DeletionPolicy": "Retain", "Properties": { "KeyPolicy": { "Statement": [ @@ -64,11 +63,11 @@ ], "Version": "2012-10-17" } - } + }, + "DeletionPolicy": "Retain" }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ @@ -85,7 +84,8 @@ } ] } - } + }, + "DeletionPolicy": "Retain" }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -286,10 +286,6 @@ } ], "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3", "EncryptionKey": { "Id": { "Fn::GetAtt": [ @@ -298,7 +294,11 @@ ] }, "Type": "KMS" - } + }, + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" } }, "DependsOn": [ @@ -306,7 +306,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventsRole46BEEA7C": { + "PipelineEventRoleCF28672B": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -333,7 +333,7 @@ } } }, - "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "PipelineEventRoleDefaultPolicy5F1B5670": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -368,10 +368,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", "Roles": [ { - "Ref": "PipelineEventsRole46BEEA7C" + "Ref": "PipelineEventRoleCF28672B" } ] } @@ -450,7 +450,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventsRole46BEEA7C", + "PipelineEventRoleCF28672B", "Arn" ] } @@ -658,7 +658,7 @@ "Arn" ] }, - "Runtime": "nodejs6.10" + "Runtime": "nodejs8.10" }, "DependsOn": [ "LambdaFunServiceRoleDefaultPolicy217FED83", @@ -666,4 +666,4 @@ ] } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts index c98510d3c392f..b5742cc222477 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts @@ -34,7 +34,7 @@ const lambdaFun = new lambda.Function(stack, 'LambdaFun', { }; `), handler: 'index.handler', - runtime: lambda.Runtime.NodeJS610, + runtime: lambda.Runtime.NodeJS810, }); const lambdaStage = pipeline.addStage({ name: 'Lambda' }); lambdaStage.addAction(new cpactions.LambdaInvokeAction({ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json index 3a89b866241ee..9f1686cc10b0e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json @@ -64,7 +64,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventsRole46BEEA7C", + "PipelineEventRoleCF28672B", "Arn" ] } @@ -358,7 +358,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventsRole46BEEA7C": { + "PipelineEventRoleCF28672B": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -385,7 +385,7 @@ } } }, - "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "PipelineEventRoleDefaultPolicy5F1B5670": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -420,10 +420,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", "Roles": [ { - "Ref": "PipelineEventsRole46BEEA7C" + "Ref": "PipelineEventRoleCF28672B" } ] } @@ -585,4 +585,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json index 9f18a6605a15b..2f1fdab425ae0 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json @@ -64,7 +64,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventsRole46BEEA7C", + "PipelineEventRoleCF28672B", "Arn" ] } @@ -72,9 +72,8 @@ ] } }, - "PipelineArtifactsBucketEncryptionKey01D58D69" : { + "PipelineArtifactsBucketEncryptionKey01D58D69": { "Type": "AWS::KMS::Key", - "DeletionPolicy": "Retain", "Properties": { "KeyPolicy": { "Statement": [ @@ -136,11 +135,11 @@ ], "Version": "2012-10-17" } - } + }, + "DeletionPolicy": "Retain" }, "PipelineArtifactsBucket22248F97": { "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ @@ -157,7 +156,8 @@ } ] } - } + }, + "DeletionPolicy": "Retain" }, "PipelineRoleD68726F7": { "Type": "AWS::IAM::Role", @@ -327,10 +327,6 @@ } ], "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3", "EncryptionKey": { "Id": { "Fn::GetAtt": [ @@ -339,7 +335,11 @@ ] }, "Type": "KMS" - } + }, + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" } }, "DependsOn": [ @@ -347,7 +347,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventsRole46BEEA7C": { + "PipelineEventRoleCF28672B": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -374,7 +374,7 @@ } } }, - "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "PipelineEventRoleDefaultPolicy5F1B5670": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -409,13 +409,13 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", "Roles": [ { - "Ref": "PipelineEventsRole46BEEA7C" + "Ref": "PipelineEventRoleCF28672B" } ] } } } -} +} \ 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 d1257fc3b4c75..bfce143ab46b3 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 @@ -155,7 +155,7 @@ "MyPipelineRoleC0D47CA4" ] }, - "MyPipelineEventsRoleFAB99F32": { + "MyPipelineEventRoleE0D393FD": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -182,7 +182,7 @@ } } }, - "MyPipelineEventsRoleDefaultPolicyF045F033": { + "MyPipelineEventRoleDefaultPolicy895F946C": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -217,10 +217,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "MyPipelineEventsRoleDefaultPolicyF045F033", + "PolicyName": "MyPipelineEventRoleDefaultPolicy895F946C", "Roles": [ { - "Ref": "MyPipelineEventsRoleFAB99F32" + "Ref": "MyPipelineEventRoleE0D393FD" } ] } @@ -277,7 +277,7 @@ "Id": "MyPipeline", "RoleArn": { "Fn::GetAtt": [ - "MyPipelineEventsRoleFAB99F32", + "MyPipelineEventRoleE0D393FD", "Arn" ] } @@ -286,4 +286,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json index c52c8a8253524..387568106ab8d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json @@ -1,8 +1,7 @@ { "Resources": { - "MyPipelineArtifactsBucketEncryptionKey8BF0A7F3" : { + "MyPipelineArtifactsBucketEncryptionKey8BF0A7F3": { "Type": "AWS::KMS::Key", - "DeletionPolicy": "Retain", "Properties": { "KeyPolicy": { "Statement": [ @@ -83,11 +82,11 @@ ], "Version": "2012-10-17" } - } + }, + "DeletionPolicy": "Retain" }, "MyPipelineArtifactsBucket727923DD": { "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ @@ -104,7 +103,8 @@ } ] } - } + }, + "DeletionPolicy": "Retain" }, "MyPipelineRoleC0D47CA4": { "Type": "AWS::IAM::Role", @@ -301,10 +301,6 @@ } ], "ArtifactStore": { - "Location": { - "Ref": "MyPipelineArtifactsBucket727923DD" - }, - "Type": "S3", "EncryptionKey": { "Id": { "Fn::GetAtt": [ @@ -313,7 +309,11 @@ ] }, "Type": "KMS" - } + }, + "Location": { + "Ref": "MyPipelineArtifactsBucket727923DD" + }, + "Type": "S3" } }, "DependsOn": [ @@ -476,10 +476,10 @@ "Id": "MyTopic", "InputTransformer": { "InputPathsMap": { - "pipeline": "$.detail.pipeline", - "state": "$.detail.state" + "f1": "$.detail.pipeline", + "f2": "$.detail.state" }, - "InputTemplate": "\"Pipeline changed state to \"" + "InputTemplate": "\"Pipeline changed state to \"" } } ] @@ -704,4 +704,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts index caee3c1040e49..e4679e355d315 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts @@ -1,6 +1,7 @@ import codebuild = require('@aws-cdk/aws-codebuild'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); +import { singletonEventRole } from './util'; /** * Start a CodeBuild build when an AWS CloudWatch events rule is triggered. @@ -16,10 +17,10 @@ export class CodeBuildProject implements events.IEventRuleTarget { return { id: this.project.node.id, arn: this.project.projectArn, - policyStatements: [new iam.PolicyStatement() + role: singletonEventRole(this.project, [new iam.PolicyStatement() .addAction('codebuild:StartBuild') .addResource(this.project.projectArn) - ], + ]), }; } } diff --git a/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts index 34b746fd702d2..8e149a71e98b1 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts @@ -1,6 +1,7 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); +import { singletonEventRole } from './util'; /** * Allows the pipeline to be used as a CloudWatch event rule target. @@ -13,9 +14,9 @@ export class CodePipeline implements events.IEventRuleTarget { return { id: this.pipeline.node.id, arn: this.pipeline.pipelineArn, - policyStatements: [new iam.PolicyStatement() + role: singletonEventRole(this.pipeline, [new iam.PolicyStatement() .addResource(this.pipeline.pipelineArn) - .addAction('codepipeline:StartPipelineExecution')] + .addAction('codepipeline:StartPipelineExecution')]) }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts index 2c12ba6f6a805..33003eea5e399 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts @@ -4,6 +4,7 @@ import events = require ('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import { Construct, Token } from '@aws-cdk/cdk'; import { ContainerOverride } from './ecs-task-properties'; +import { singletonEventRole } from './util'; /** * Properties to define an EC2 Event Task @@ -94,7 +95,7 @@ export class EcsEc2Task implements events.IEventRuleTarget { return { id: this.taskDefinition.node.id + ' on ' + this.cluster.node.id, arn: this.cluster.clusterArn, - policyStatements, + role: singletonEventRole(this.taskDefinition, policyStatements), ecsParameters: { taskCount: this.taskCount, taskDefinitionArn: this.taskDefinition.taskDefinitionArn diff --git a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts index 4de4aa72197e8..5fc31313a6425 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts @@ -1,6 +1,7 @@ import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import sfn = require('@aws-cdk/aws-stepfunctions'); +import { singletonEventRole } from './util'; /** * Customize the Step Functions State Machine target @@ -30,10 +31,10 @@ export class SfnStateMachine implements events.IEventRuleTarget { return { id: this.machine.node.id, arn: this.machine.stateMachineArn, - policyStatements: [new iam.PolicyStatement() + role: singletonEventRole(this.machine, [new iam.PolicyStatement() .addAction('states:StartExecution') .addResource(this.machine.stateMachineArn) - ], + ]), input: this.props.input }; } diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts new file mode 100644 index 0000000000000..34844e2b854ef --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -0,0 +1,22 @@ +import iam = require('@aws-cdk/aws-iam'); +import { Construct, IConstruct } from "@aws-cdk/cdk"; + +/** + * Obtain the Role for the CloudWatch event + * + * If a role already exists, it will be returned. This ensures that if multiple + * events have the same target, they will share a role. + */ +export function singletonEventRole(scope: IConstruct, policyStatements: iam.PolicyStatement[]): iam.IRole { + const id = 'EventRole'; + const existing = scope.node.tryFindChild(id) as iam.IRole; + if (existing) { return existing; } + + const role = new iam.Role(scope as Construct, id, { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com') + }); + + policyStatements.forEach(role.addToPolicy.bind(role)); + + return role; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index 6f46fbbd07e6f..39fadf5e4b2b1 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -8,7 +8,7 @@ test('use codebuild project as an eventrule target', () => { // GIVEN const stack = new Stack(); const project = new codebuild.Project(stack, 'MyProject', { source: new codebuild.CodePipelineSource() }); - const rule = new events.EventRule(stack, 'rule', { scheduleExpression: 'rate(1 min)' }); + const rule = new events.EventRule(stack, 'Rule', { scheduleExpression: 'rate(1 min)' }); // WHEN rule.addTarget(new targets.CodeBuildProject(project)); @@ -26,7 +26,7 @@ test('use codebuild project as an eventrule target', () => { Id: "MyProject", RoleArn: { "Fn::GetAtt": [ - "ruleRoleMyProject4A80A7E9", + "MyProjectEventRole0D0257FA", "Arn" ] } diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json index 6b3205bdc56f3..929065e6099a7 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json @@ -47,7 +47,7 @@ "Id": "MyProject", "RoleArn": { "Fn::GetAtt": [ - "MyRepoOnCommitRoleMyProject127015C1", + "MyProjectEventRole0D0257FA", "Arn" ] } @@ -68,7 +68,7 @@ ] } }, - "MyRepoOnCommitRoleMyProject127015C1": { + "MyProjectEventRole0D0257FA": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -95,7 +95,7 @@ } } }, - "MyRepoOnCommitRoleMyProjectDefaultPolicy033A0114": { + "MyProjectEventRoleDefaultPolicyC8F95DF7": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -113,10 +113,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "MyRepoOnCommitRoleMyProjectDefaultPolicy033A0114", + "PolicyName": "MyProjectEventRoleDefaultPolicyC8F95DF7", "Roles": [ { - "Ref": "MyRepoOnCommitRoleMyProject127015C1" + "Ref": "MyProjectEventRole0D0257FA" } ] } @@ -417,4 +417,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts index 6c883192911e1..857efbea1f8b7 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts @@ -55,7 +55,7 @@ test('use codebuild project as an eventrule target', () => { { Arn: pipelineArn, Id: "Pipeline", - RoleArn: { "Fn::GetAtt": [ "ruleRolePipelineFB6AC329", "Arn" ] } + RoleArn: { "Fn::GetAtt": [ "PipelineEventRoleCF28672B", "Arn" ] } } ] })); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts index 1e7589cf1e92f..34bc7acc2e21d 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts @@ -50,7 +50,7 @@ test("Can use EC2 taskdef as EventRule target", () => { }, InputTemplate: "{\"containerOverrides\":[{\"containerName\":\"TheContainer\",\"command\":[\"echo\",]}]}" }, - RoleArn: { "Fn::GetAtt": ["RuleRoleTaskDefonEcsClusterE14C6DF4", "Arn"] } + RoleArn: { "Fn::GetAtt": ["TaskDefEventRole0B9B1C61", "Arn"] } } ] }); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json index ccc8f09fc184b..5670d561c900c 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json @@ -1055,7 +1055,7 @@ "Input": "{\"containerOverrides\":[{\"containerName\":\"TheContainer\",\"environment\":[{\"name\":\"I_WAS_TRIGGERED\",\"value\":\"From CloudWatch Events\"}]}]}", "RoleArn": { "Fn::GetAtt": [ - "RuleRoleTaskDefonEcsClusterE14C6DF4", + "TaskDefEventRole0B9B1C61", "Arn" ] } @@ -1063,7 +1063,7 @@ ] } }, - "RuleRoleTaskDefonEcsClusterE14C6DF4": { + "TaskDefEventRole0B9B1C61": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -1090,7 +1090,7 @@ } } }, - "RuleRoleTaskDefonEcsClusterDefaultPolicy8B2DC915": { + "TaskDefEventRoleDefaultPolicy97768884": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -1125,10 +1125,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "RuleRoleTaskDefonEcsClusterDefaultPolicy8B2DC915", + "PolicyName": "TaskDefEventRoleDefaultPolicy97768884", "Roles": [ { - "Ref": "RuleRoleTaskDefonEcsClusterE14C6DF4" + "Ref": "TaskDefEventRole0B9B1C61" } ] } @@ -1148,4 +1148,4 @@ "Description": "S3 key for asset version \"aws-ecs-integ-ecs/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 3cc6801d3068b..be54521300283 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,4 +1,3 @@ -import iam = require('@aws-cdk/aws-iam'); import { CfnOutput, Construct, Resource, Token } from '@aws-cdk/cdk'; import { EventPattern } from './event-pattern'; import { CfnRule } from './events.generated'; @@ -132,23 +131,15 @@ export class EventRule extends Resource implements IEventRule { throw new Error('Duplicate event rule target with ID: ' + id); } - let roleArn; - if ((targetProps.policyStatements || []).length > 0) { - const role = new iam.Role(this, `Role${targetProps.id}`, { - assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), - }); - - for (const statement of targetProps.policyStatements || []) { - role.addToPolicy(statement); - } - - roleArn = role.roleArn; - } + const roleArn = targetProps.role ? targetProps.role.roleArn : undefined; this.targets.push({ - ...targetProps, id, + arn: targetProps.arn, roleArn, + ecsParameters: targetProps.ecsParameters, + kinesisParameters: targetProps.kinesisParameters, + runCommandParameters: targetProps.runCommandParameters, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, inputTransformer: inputProps && inputProps.inputTemplate !== undefined ? { diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index 6cca20a9623c0..18ad6ddf8d205 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -33,9 +33,9 @@ export interface EventRuleTargetProperties { readonly arn: string; /** - * Policy statements to add to the event's role + * Role to use to invoke this event target */ - readonly policyStatements?: iam.PolicyStatement[]; + readonly role?: iam.IRole; /** * The Amazon ECS task definition and task count to use, if the event target diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index f3c4ba93a5e0b..324d30a00d830 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -1,5 +1,6 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); +import { ServicePrincipal } from '@aws-cdk/aws-iam'; import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; @@ -293,20 +294,22 @@ export = { test.done(); }, - 'target can declare policy statements which will be added to role'(test: Test) { + 'target can declare role which will be used'(test: Test) { // GIVEN const stack = new cdk.Stack(); const rule = new EventRule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); + const role = new iam.Role(stack, 'SomeRole', { + assumedBy: new ServicePrincipal('nobody') + }); + // a plain string should just be stringified (i.e. double quotes added and escaped) rule.addTarget({ bind: () => ({ id: 'T2', arn: 'ARN2', - policyStatements: [new iam.PolicyStatement() - .addAction('some:action') - .addResource('ARN2')] + role, }) }); @@ -316,24 +319,11 @@ export = { { "Arn": "ARN2", "Id": "T2", - "RoleArn": {"Fn::GetAtt": ["EventRuleRoleT2D4DAD5B9", "Arn"]} + "RoleArn": {"Fn::GetAtt": ["SomeRole6DDC54DD", "Arn"]} } ] })); - expect(stack).to(haveResource('AWS::IAM::Policy', { - "PolicyDocument": { - "Statement": [ - { - "Action": "some:action", - "Effect": "Allow", - "Resource": "ARN2" - } - ], - "Version": "2012-10-17" - }, - })); - test.done(); }, diff --git a/tools/cdk-integ-tools/bin/cdk-integ-assert.ts b/tools/cdk-integ-tools/bin/cdk-integ-assert.ts index df68a688598e1..993d3f7d6260c 100644 --- a/tools/cdk-integ-tools/bin/cdk-integ-assert.ts +++ b/tools/cdk-integ-tools/bin/cdk-integ-assert.ts @@ -38,7 +38,7 @@ async function main() { if (failures.length > 0) { // tslint:disable-next-line:max-line-length - throw new Error(`The following integ stacks have changed: ${failures.join(', ')}. Run 'npm run integ' to verify that everything still deploys.`); + throw new Error(`Some stacks have changed. To verify that they still deploy successfully, run: 'npm run integ ${failures.join(' ')}'`); } } From f8ba529a3592c600c28b7eda990b6b0618b50bdb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 16:41:19 +0200 Subject: [PATCH 23/33] Update another expectation --- packages/decdk/test/__snapshots__/synth.test.js.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index a89d53a44d62d..787ea7b9d7012 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -2163,7 +2163,7 @@ Object { }, "Type": "AWS::IAM::Policy", }, - "PipelineEventsRole46BEEA7C": Object { + "PipelineEventRoleCF28672B": Object { "Properties": Object { "AssumeRolePolicyDocument": Object { "Statement": Array [ @@ -2190,7 +2190,7 @@ Object { }, "Type": "AWS::IAM::Role", }, - "PipelineEventsRoleDefaultPolicyFF4FCCE0": Object { + "PipelineEventRoleDefaultPolicy5F1B5670": Object { "Properties": Object { "PolicyDocument": Object { "Statement": Array [ @@ -2224,10 +2224,10 @@ Object { ], "Version": "2012-10-17", }, - "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", "Roles": Array [ Object { - "Ref": "PipelineEventsRole46BEEA7C", + "Ref": "PipelineEventRoleCF28672B", }, ], }, @@ -2460,7 +2460,7 @@ Object { "Id": "Pipeline", "RoleArn": Object { "Fn::GetAtt": Array [ - "PipelineEventsRole46BEEA7C", + "PipelineEventRoleCF28672B", "Arn", ], }, From fa04166fbf294adc01c8d79ea8962f692bba30c5 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 16:49:25 +0200 Subject: [PATCH 24/33] Token JSONification no longer fails --- .../@aws-cdk/cdk/test/test.cloudformation-json.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts index 6167d2c5e771d..64e5239d25142 100644 --- a/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/test.cloudformation-json.ts @@ -3,18 +3,6 @@ import { CloudFormationLang, Fn, Stack, Token } from '../lib'; import { evaluateCFN } from './evaluate-cfn'; export = { - 'plain JSON.stringify() on a Token fails'(test: Test) { - // GIVEN - const token = new Token(() => 'value'); - - // WHEN - test.throws(() => { - JSON.stringify({ token }); - }); - - test.done(); - }, - 'string tokens can be JSONified and JSONification can be reversed'(test: Test) { const stack = new Stack(); From 4ccf95b68c1ae190198b5c9cc436669164f14ba1 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 17:05:10 +0200 Subject: [PATCH 25/33] Undo default change --- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 51b8a9dcbd36f..07549dd7be376 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -222,7 +222,7 @@ export interface AddS3EventSelectorOptions { /** * Specifies whether to log read-only events, write-only events, or all events. * - * @default ReadWriteType.Write + * @default ReadWriteType.All */ readonly readWriteType?: ReadWriteType; From 02a1451657299cb54086bd2ea1295deeb54913e3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 17:05:33 +0200 Subject: [PATCH 26/33] Also undo S3Selector change --- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 07549dd7be376..41d21b9ab3345 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -182,7 +182,7 @@ export class CloudTrail extends cdk.Construct { * @param prefixes the list of object ARN prefixes to include in logging (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addS3EventSelector(prefixes: string[] = [], options: AddS3EventSelectorOptions = {}) { + public addS3EventSelector(prefixes: string[], options: AddS3EventSelectorOptions = {}) { if (prefixes.length > 250) { throw new Error("A maximum of 250 data elements can be in one event selector"); } From 22e588c7c78aa58c09bc653be39570bba48e8e84 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 17:06:31 +0200 Subject: [PATCH 27/33] Undo test change --- .../@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts index a187dbb427ac6..79b53ac3392e7 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-and-dashboard.ts @@ -17,7 +17,7 @@ const queue = new cdk.CfnResource(stack, 'queue', { type: 'AWS::SQS::Queue' }); const metric = new cloudwatch.Metric({ namespace: 'AWS/SQS', metricName: 'ApproximateNumberOfMessagesVisible', - dimensions: { QueueName: queue.getAtt('QueueName').toString() } + dimensions: { QueueName: queue.getAtt('QueueName') } }); const alarm = metric.newAlarm(stack, 'Alarm', { From f79599c88050443a38e9a68fdeef015cf102b4eb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 May 2019 21:51:59 +0200 Subject: [PATCH 28/33] ID rename back to plural --- ...a-deployed-through-codepipeline.lit.expected.json | 12 ++++++------ .../test/integ.lambda-pipeline.expected.json | 10 +++++----- ...-code-build-multiple-inputs-outputs.expected.json | 10 +++++----- .../test/integ.pipeline-code-commit.expected.json | 10 +++++----- .../test/integ.pipeline-ecr-source.expected.json | 10 +++++----- packages/@aws-cdk/aws-events-targets/lib/util.ts | 2 +- .../test/codebuild/codebuild.test.ts | 2 +- .../codebuild/integ.project-events.expected.json | 10 +++++----- .../test/codepipeline/pipeline.test.ts | 2 +- .../test/ecs/ec2-event-rule-target.test.ts | 2 +- .../test/ecs/integ.event-task.lit.expected.json | 10 +++++----- 11 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json index 8229c2b3b932a..caed1d048c69b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json @@ -500,7 +500,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventRoleCF28672B": { + "PipelineEventsRole46BEEA7C": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -527,7 +527,7 @@ } } }, - "PipelineEventRoleDefaultPolicy5F1B5670": { + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -562,10 +562,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", "Roles": [ { - "Ref": "PipelineEventRoleCF28672B" + "Ref": "PipelineEventsRole46BEEA7C" } ] } @@ -682,7 +682,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventRoleCF28672B", + "PipelineEventsRole46BEEA7C", "Arn" ] } @@ -754,7 +754,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventRoleCF28672B", + "PipelineEventsRole46BEEA7C", "Arn" ] } 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 b13550f0311b1..66b99d6d54d05 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 @@ -306,7 +306,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventRoleCF28672B": { + "PipelineEventsRole46BEEA7C": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -333,7 +333,7 @@ } } }, - "PipelineEventRoleDefaultPolicy5F1B5670": { + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -368,10 +368,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", "Roles": [ { - "Ref": "PipelineEventRoleCF28672B" + "Ref": "PipelineEventsRole46BEEA7C" } ] } @@ -450,7 +450,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventRoleCF28672B", + "PipelineEventsRole46BEEA7C", "Arn" ] } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json index 9f1686cc10b0e..8959ea6c3fd74 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json @@ -64,7 +64,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventRoleCF28672B", + "PipelineEventsRole46BEEA7C", "Arn" ] } @@ -358,7 +358,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventRoleCF28672B": { + "PipelineEventsRole46BEEA7C": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -385,7 +385,7 @@ } } }, - "PipelineEventRoleDefaultPolicy5F1B5670": { + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -420,10 +420,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", "Roles": [ { - "Ref": "PipelineEventRoleCF28672B" + "Ref": "PipelineEventsRole46BEEA7C" } ] } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json index 2f1fdab425ae0..8e3dae70f0b26 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit.expected.json @@ -64,7 +64,7 @@ "Id": "Pipeline", "RoleArn": { "Fn::GetAtt": [ - "PipelineEventRoleCF28672B", + "PipelineEventsRole46BEEA7C", "Arn" ] } @@ -347,7 +347,7 @@ "PipelineRoleD68726F7" ] }, - "PipelineEventRoleCF28672B": { + "PipelineEventsRole46BEEA7C": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -374,7 +374,7 @@ } } }, - "PipelineEventRoleDefaultPolicy5F1B5670": { + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -409,10 +409,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", "Roles": [ { - "Ref": "PipelineEventRoleCF28672B" + "Ref": "PipelineEventsRole46BEEA7C" } ] } 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 bfce143ab46b3..9c3df28286ac0 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 @@ -155,7 +155,7 @@ "MyPipelineRoleC0D47CA4" ] }, - "MyPipelineEventRoleE0D393FD": { + "MyPipelineEventsRoleFAB99F32": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -182,7 +182,7 @@ } } }, - "MyPipelineEventRoleDefaultPolicy895F946C": { + "MyPipelineEventsRoleDefaultPolicyF045F033": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -217,10 +217,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "MyPipelineEventRoleDefaultPolicy895F946C", + "PolicyName": "MyPipelineEventsRoleDefaultPolicyF045F033", "Roles": [ { - "Ref": "MyPipelineEventRoleE0D393FD" + "Ref": "MyPipelineEventsRoleFAB99F32" } ] } @@ -277,7 +277,7 @@ "Id": "MyPipeline", "RoleArn": { "Fn::GetAtt": [ - "MyPipelineEventRoleE0D393FD", + "MyPipelineEventsRoleFAB99F32", "Arn" ] } diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index 34844e2b854ef..1b0215589ff9f 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -8,7 +8,7 @@ import { Construct, IConstruct } from "@aws-cdk/cdk"; * events have the same target, they will share a role. */ export function singletonEventRole(scope: IConstruct, policyStatements: iam.PolicyStatement[]): iam.IRole { - const id = 'EventRole'; + const id = 'EventsRole'; const existing = scope.node.tryFindChild(id) as iam.IRole; if (existing) { return existing; } diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index 39fadf5e4b2b1..a16371beed75d 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -26,7 +26,7 @@ test('use codebuild project as an eventrule target', () => { Id: "MyProject", RoleArn: { "Fn::GetAtt": [ - "MyProjectEventRole0D0257FA", + "MyProjectEventsRole5B7D93F5", "Arn" ] } diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json index 929065e6099a7..04a6e3740851e 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json @@ -47,7 +47,7 @@ "Id": "MyProject", "RoleArn": { "Fn::GetAtt": [ - "MyProjectEventRole0D0257FA", + "MyProjectEventsRole5B7D93F5", "Arn" ] } @@ -68,7 +68,7 @@ ] } }, - "MyProjectEventRole0D0257FA": { + "MyProjectEventsRole5B7D93F5": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -95,7 +95,7 @@ } } }, - "MyProjectEventRoleDefaultPolicyC8F95DF7": { + "MyProjectEventsRoleDefaultPolicy397DCBF8": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -113,10 +113,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "MyProjectEventRoleDefaultPolicyC8F95DF7", + "PolicyName": "MyProjectEventsRoleDefaultPolicy397DCBF8", "Roles": [ { - "Ref": "MyProjectEventRole0D0257FA" + "Ref": "MyProjectEventsRole5B7D93F5" } ] } diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts index 857efbea1f8b7..a990b69d95fef 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts @@ -55,7 +55,7 @@ test('use codebuild project as an eventrule target', () => { { Arn: pipelineArn, Id: "Pipeline", - RoleArn: { "Fn::GetAtt": [ "PipelineEventRoleCF28672B", "Arn" ] } + RoleArn: { "Fn::GetAtt": [ "PipelineEventsRole46BEEA7C", "Arn" ] } } ] })); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts index 34bc7acc2e21d..d8f844335d3bf 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts @@ -50,7 +50,7 @@ test("Can use EC2 taskdef as EventRule target", () => { }, InputTemplate: "{\"containerOverrides\":[{\"containerName\":\"TheContainer\",\"command\":[\"echo\",]}]}" }, - RoleArn: { "Fn::GetAtt": ["TaskDefEventRole0B9B1C61", "Arn"] } + RoleArn: { "Fn::GetAtt": ["TaskDefEventsRoleFB3B67B8", "Arn"] } } ] }); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json index 5670d561c900c..279d3274ae13e 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json @@ -1055,7 +1055,7 @@ "Input": "{\"containerOverrides\":[{\"containerName\":\"TheContainer\",\"environment\":[{\"name\":\"I_WAS_TRIGGERED\",\"value\":\"From CloudWatch Events\"}]}]}", "RoleArn": { "Fn::GetAtt": [ - "TaskDefEventRole0B9B1C61", + "TaskDefEventsRoleFB3B67B8", "Arn" ] } @@ -1063,7 +1063,7 @@ ] } }, - "TaskDefEventRole0B9B1C61": { + "TaskDefEventsRoleFB3B67B8": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -1090,7 +1090,7 @@ } } }, - "TaskDefEventRoleDefaultPolicy97768884": { + "TaskDefEventsRoleDefaultPolicyA124E85B": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -1125,10 +1125,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TaskDefEventRoleDefaultPolicy97768884", + "PolicyName": "TaskDefEventsRoleDefaultPolicyA124E85B", "Roles": [ { - "Ref": "TaskDefEventRole0B9B1C61" + "Ref": "TaskDefEventsRoleFB3B67B8" } ] } From dd8322f689ebac761a7928c3df8ba478c9b7d96c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sun, 19 May 2019 14:48:11 +0200 Subject: [PATCH 29/33] Update decdk snapshot --- packages/decdk/test/__snapshots__/synth.test.js.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index 787ea7b9d7012..a89d53a44d62d 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -2163,7 +2163,7 @@ Object { }, "Type": "AWS::IAM::Policy", }, - "PipelineEventRoleCF28672B": Object { + "PipelineEventsRole46BEEA7C": Object { "Properties": Object { "AssumeRolePolicyDocument": Object { "Statement": Array [ @@ -2190,7 +2190,7 @@ Object { }, "Type": "AWS::IAM::Role", }, - "PipelineEventRoleDefaultPolicy5F1B5670": Object { + "PipelineEventsRoleDefaultPolicyFF4FCCE0": Object { "Properties": Object { "PolicyDocument": Object { "Statement": Array [ @@ -2224,10 +2224,10 @@ Object { ], "Version": "2012-10-17", }, - "PolicyName": "PipelineEventRoleDefaultPolicy5F1B5670", + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", "Roles": Array [ Object { - "Ref": "PipelineEventRoleCF28672B", + "Ref": "PipelineEventsRole46BEEA7C", }, ], }, @@ -2460,7 +2460,7 @@ Object { "Id": "Pipeline", "RoleArn": Object { "Fn::GetAtt": Array [ - "PipelineEventRoleCF28672B", + "PipelineEventsRole46BEEA7C", "Arn", ], }, From 42673c087e4c528eaf2567179cf5c966b7844dee Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 May 2019 13:25:38 +0200 Subject: [PATCH 30/33] Fix some build --- packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts | 2 +- .../aws-events-targets/test/ecs/ec2-event-rule-target.test.ts | 2 +- packages/@aws-cdk/aws-events/lib/rule.ts | 2 +- packages/@aws-cdk/cdk/lib/resolve.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index 19e345b0f9a8c..facae5fe9003b 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -194,7 +194,7 @@ export = { 'add an event rule'(test: Test) { // GIVEN const stack = getTestStack(); - const trail = new CloudTrail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WriteOnly }); + const trail = new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WriteOnly }); // WHEN trail.onEvent('DoEvents', { diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts index d8f844335d3bf..598dddc4188f2 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts @@ -8,7 +8,7 @@ import targets = require('../../lib'); test("Can use EC2 taskdef as EventRule target", () => { // GIVEN const stack = new cdk.Stack(); - const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAZs: 1 }); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index fea722a1d0970..d5c01804b1898 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,7 +1,7 @@ import { Construct, Resource, Token } from '@aws-cdk/cdk'; import { EventPattern } from './event-pattern'; import { CfnRule } from './events.generated'; -import { EventRuleAttributes, IEventRule } from './rule-ref'; +import { IEventRule } from './rule-ref'; import { IEventRuleTarget } from './target'; import { mergeEventPattern } from './util'; diff --git a/packages/@aws-cdk/cdk/lib/resolve.ts b/packages/@aws-cdk/cdk/lib/resolve.ts index 924c6f2f521ed..0f9bf51a10704 100644 --- a/packages/@aws-cdk/cdk/lib/resolve.ts +++ b/packages/@aws-cdk/cdk/lib/resolve.ts @@ -89,7 +89,7 @@ export function resolve(obj: any, options: IResolveOptions): any { // number - potentially decode Tokenized number // if (typeof(obj) === 'number') { - return resolveNumberToken(obj, context); + return resolveNumberToken(obj, makeContext()); } // From 5127d227acd3b500a76c954c5144ed630ca51b7f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 May 2019 13:28:06 +0200 Subject: [PATCH 31/33] Update ECS README --- packages/@aws-cdk/aws-ecs/README.md | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index a6eed1991e3cf..34a9caa093ce6 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -305,26 +305,19 @@ const rule = new events.EventRule(this, 'Rule', { scheduleExpression: 'rate(1 minute)', }); -// Use as the target of the EventRule -const target = new targets.Ec2EventRuleTarget(this, 'EventTarget', { +// Pass an environment variable to the container 'TheContainer' in the task +rule.addTarget(new targets.EcsEc2Task({ cluster, taskDefinition, - taskCount: 1 -}); - -// Pass an environment variable to the container 'TheContainer' in the task -rule.addTarget(target, { - jsonTemplate: JSON.stringify({ - containerOverrides: [{ - name: 'TheContainer', - environment: [{ name: 'I_WAS_TRIGGERED', value: 'From CloudWatch Events' }] + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + environment: [{ + name: 'I_WAS_TRIGGERED', + value: 'From CloudWatch Events' }] - }) -}); + }] +})); ``` -> Note: it is currently not possible to start AWS Fargate tasks in this way. - -## Roadmap - -- [ ] Service Discovery Integration +> Note: it is currently not possible to start AWS Fargate tasks in this way. \ No newline at end of file From c55c0488dd17de28af38562bfc9783658ef9e839 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 May 2019 14:19:13 +0200 Subject: [PATCH 32/33] Simplify names --- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 4 +- .../@aws-cdk/aws-codebuild/lib/project.ts | 24 +++++----- .../@aws-cdk/aws-codecommit/lib/repository.ts | 38 +++++++-------- .../cloudformation/test.pipeline-actions.ts | 8 ++-- .../test/integ.pipeline-events.ts | 2 +- .../@aws-cdk/aws-codepipeline/lib/action.ts | 8 ++-- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 4 +- .../@aws-cdk/aws-codepipeline/lib/stage.ts | 4 +- packages/@aws-cdk/aws-ecr/lib/repository.ts | 10 ++-- packages/@aws-cdk/aws-ecs/README.md | 8 ++-- .../aws-events-targets/lib/codebuild.ts | 4 +- .../aws-events-targets/lib/codepipeline.ts | 6 +-- .../aws-events-targets/lib/ecs-ec2-task.ts | 8 ++-- .../@aws-cdk/aws-events-targets/lib/lambda.ts | 6 +-- .../@aws-cdk/aws-events-targets/lib/sns.ts | 6 +-- .../aws-events-targets/lib/state-machine.ts | 10 ++-- .../test/codebuild/codebuild.test.ts | 2 +- .../test/codebuild/integ.project-events.ts | 4 +- .../test/codepipeline/pipeline.test.ts | 4 +- .../test/ecs/ec2-event-rule-target.test.ts | 2 +- .../test/ecs/integ.event-task.lit.ts | 6 +-- .../test/lambda/integ.events.ts | 4 +- .../test/lambda/lambda.test.ts | 4 +- .../test/sns/integ.sns-event-rule-target.ts | 2 +- .../aws-events-targets/test/sns/sns.test.ts | 4 +- .../test/stepfunctions/statemachine.test.ts | 4 +- packages/@aws-cdk/aws-events/README.md | 6 +-- packages/@aws-cdk/aws-events/lib/input.ts | 28 +++++------ packages/@aws-cdk/aws-events/lib/rule-ref.ts | 2 +- packages/@aws-cdk/aws-events/lib/rule.ts | 18 +++---- packages/@aws-cdk/aws-events/lib/target.ts | 12 ++--- packages/@aws-cdk/aws-events/package.json | 6 +++ .../@aws-cdk/aws-events/test/test.input.ts | 24 +++++----- .../@aws-cdk/aws-events/test/test.rule.ts | 48 +++++++++---------- packages/@aws-cdk/aws-s3/lib/bucket.ts | 8 ++-- 35 files changed, 172 insertions(+), 166 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 05d49992c614e..8eee3364e6ebd 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -217,8 +217,8 @@ export class Trail extends Resource { * Note that the event doesn't necessarily have to come from this * trail. Be sure to filter the event properly using an event pattern. */ - public onEvent(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { - const rule = new events.EventRule(this, name, options); + public onEvent(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { + const rule = new events.Rule(this, name, options); rule.addTarget(target); rule.addEventPattern({ detailType: ['AWS API Call via CloudTrail'] diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 9b42ade4ee103..47f2b451b97f0 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -57,7 +57,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.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines a CloudWatch event rule that triggers upon phase change of this @@ -65,22 +65,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.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onPhaseChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines an event rule which triggers when a build starts. */ - onBuildStarted(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onBuildStarted(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines an event rule which triggers when a build fails. */ - onBuildFailed(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onBuildFailed(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines an event rule which triggers when a build completes successfully. */ - onBuildSucceeded(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onBuildSucceeded(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * @returns a CloudWatch metric associated with this build project. @@ -182,8 +182,8 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { - const rule = new events.EventRule(this, name, options); + public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { + const rule = new events.Rule(this, name, options); rule.addTarget(target); rule.addEventPattern({ source: ['aws.codebuild'], @@ -203,8 +203,8 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { - const rule = new events.EventRule(this, name, options); + public onPhaseChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { + const rule = new events.Rule(this, name, options); rule.addTarget(target); rule.addEventPattern({ source: ['aws.codebuild'], @@ -224,7 +224,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onBuildStarted(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onStateChange(name, target, options); rule.addEventPattern({ detail: { @@ -240,7 +240,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onBuildFailed(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onStateChange(name, target, options); rule.addEventPattern({ detail: { @@ -256,7 +256,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onBuildSucceeded(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onStateChange(name, target, options); rule.addEventPattern({ detail: { diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 3578293c72b20..efa5a0afc3910 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -31,53 +31,53 @@ 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.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onEvent(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines a CloudWatch event rule which triggers when a "CodeCommit * Repository State Change" event occurs. */ - onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): 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.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onReferenceCreated(name: string, target?: events.IRuleTarget, options?: events.RuleProps): 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.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onReferenceUpdated(name: string, target?: events.IRuleTarget, options?: events.RuleProps): 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.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onReferenceDeleted(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines a CloudWatch event rule which triggers when a pull request state is changed. */ - onPullRequestStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onPullRequestStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines a CloudWatch event rule which triggers when a comment is made on a pull request. */ - onCommentOnPullRequest(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onCommentOnPullRequest(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; /** * Defines a CloudWatch event rule which triggers when a comment is made on a commit. */ - onCommentOnCommit(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onCommentOnCommit(name: string, target?: events.IRuleTarget, options?: events.RuleProps): 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.IEventRuleTarget, branch?: string): events.EventRule; + onCommit(name: string, target?: events.IRuleTarget, branch?: string): events.Rule; } /** @@ -106,8 +106,8 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { - const rule = new events.EventRule(this, name, options); + public onEvent(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { + const rule = new events.Rule(this, name, options); rule.addEventPattern({ source: [ 'aws.codecommit' ], resources: [ this.repositoryArn ] @@ -120,7 +120,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onEvent(name, target, options); rule.addEventPattern({ detailType: [ 'CodeCommit Repository State Change' ], @@ -132,7 +132,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onReferenceCreated(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onStateChange(name, target, options); rule.addEventPattern({ detail: { event: [ 'referenceCreated' ] } }); return rule; @@ -142,7 +142,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onReferenceUpdated(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onStateChange(name, target, options); rule.addEventPattern({ detail: { event: [ 'referenceCreated', 'referenceUpdated' ] } }); return rule; @@ -152,7 +152,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onReferenceDeleted(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onStateChange(name, target, options); rule.addEventPattern({ detail: { event: [ 'referenceDeleted' ] } }); return rule; @@ -161,7 +161,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onPullRequestStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onEvent(name, target, options); rule.addEventPattern({ detailType: [ 'CodeCommit Pull Request State Change' ] }); return rule; @@ -170,7 +170,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onCommentOnPullRequest(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onEvent(name, target, options); rule.addEventPattern({ detailType: [ 'CodeCommit Comment on Pull Request' ] }); return rule; @@ -179,7 +179,7 @@ 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.IEventRuleTarget, options?: events.EventRuleProps) { + public onCommentOnCommit(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { const rule = this.onEvent(name, target, options); rule.addEventPattern({ detailType: [ 'CodeCommit Comment on Commit' ] }); return rule; @@ -190,7 +190,7 @@ abstract class RepositoryBase extends Resource implements IRepository { * @param target The target of the event * @param branch The branch to monitor. Defaults to all branches. */ - public onCommit(name: string, target?: events.IEventRuleTarget, branch?: string) { + public onCommit(name: string, target?: events.IRuleTarget, branch?: string) { const rule = this.onReferenceUpdated(name, target); if (branch) { rule.addEventPattern({ detail: { referenceName: [ branch ] }}); 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 bb436b35d6889..656f96b26de24 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 @@ -313,8 +313,8 @@ class PipelineDouble extends cdk.Construct implements codepipeline.IPipeline { this.role = role; } - public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { - throw new Error('asEventRuleTarget() is unsupported in PipelineDouble'); + public bind(_rule: events.IRule): events.RuleTargetProperties { + throw new Error('asRuleTarget() is unsupported in PipelineDouble'); } public grantBucketRead(_identity?: iam.IGrantable): iam.Grant { @@ -356,8 +356,8 @@ class StageDouble implements codepipeline.IStage { throw new Error('addAction() is not supported on StageDouble'); } - public onStateChange(_name: string, _target?: events.IEventRuleTarget, _options?: events.EventRuleProps): - events.EventRule { + public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): + events.Rule { throw new Error('onStateChange() is not supported on StageDouble'); } } 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 eb1e40e1930da..328bb6d6c293f 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 @@ -49,7 +49,7 @@ 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.EventTargetInput.fromText(`Pipeline ${eventPipeline} changed state to ${eventState}`), + 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/lib/action.ts b/packages/@aws-cdk/aws-codepipeline/lib/action.ts index ef298860331ee..e8dabace20812 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/action.ts @@ -55,7 +55,7 @@ export interface ActionBind { /** * The abstract view of an AWS CodePipeline as required and used by Actions. - * It extends {@link events.IEventRuleTarget}, + * It extends {@link events.IRuleTarget}, * so this interface can be used as a Target for CloudWatch Events. */ export interface IPipeline extends IResource { @@ -99,7 +99,7 @@ export interface IStage { addAction(action: Action): void; - onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule; + onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; } /** @@ -240,8 +240,8 @@ export abstract class Action { } } - public onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { - const rule = new events.EventRule(this.scope, name, options); + public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps) { + const rule = new events.Rule(this.scope, name, options); rule.addTarget(target); rule.addEventPattern({ detailType: [ 'CodePipeline Stage Execution State Change' ], diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 2868e31c76636..82662424d94a5 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -278,8 +278,8 @@ export class Pipeline extends PipelineBase { * more than a single onStateChange event, you will need to explicitly * specify a name. */ - public onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule { - const rule = new events.EventRule(this, name, options); + 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' ], diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 029d894ea89f0..b3a22f5b78cab 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -78,8 +78,8 @@ export class Stage implements IStage { this.attachActionToPipeline(action); } - public onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps): events.EventRule { - const rule = new events.EventRule(this.scope, name, options); + public onStateChange(name: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { + const rule = new events.Rule(this.scope, name, options); rule.addTarget(target); rule.addEventPattern({ detailType: [ 'CodePipeline Stage Execution State Change' ], diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 42e1f35cf659b..658c87c15a876 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -62,10 +62,10 @@ export interface IRepository extends IResource { * 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 IEventRuleTarget to invoke when this event happens (you can add more targets using `addTarget`) + * @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 */ - onImagePushed(name: string, target?: events.IEventRuleTarget, imageTag?: string): events.EventRule; + onImagePushed(name: string, target?: events.IRuleTarget, imageTag?: string): events.Rule; } /** @@ -114,11 +114,11 @@ export abstract class RepositoryBase extends Resource implements IRepository { * 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 IEventRuleTarget to invoke when this event happens (you can add more targets using `addTarget`) + * @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 */ - public onImagePushed(name: string, target?: events.IEventRuleTarget, imageTag?: string): events.EventRule { - return new events.EventRule(this, name, { + 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'], diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 34a9caa093ce6..fc6aff6090040 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -285,7 +285,7 @@ you can configure on your instances. ## Integration with CloudWatch Events To start an Amazon ECS task on an Amazon EC2-backed Cluster, instantiate an -`Ec2TaskEventRuleTarget` instead of an `Ec2Service`: +`@aws-cdk/aws-events-targets.EcsEc2Task` instead of an `Ec2Service`: ```ts import targets = require('@aws-cdk/aws-events-targets'); @@ -300,8 +300,8 @@ taskDefinition.addContainer('TheContainer', { logging: new ecs.AwsLogDriver(this, 'TaskLogging', { streamPrefix: 'EventDemo' }) }); -// An EventRule that describes the event trigger (in this case a scheduled run) -const rule = new events.EventRule(this, 'Rule', { +// An Rule that describes the event trigger (in this case a scheduled run) +const rule = new events.Rule(this, 'Rule', { scheduleExpression: 'rate(1 minute)', }); @@ -320,4 +320,4 @@ rule.addTarget(new targets.EcsEc2Task({ })); ``` -> Note: it is currently not possible to start AWS Fargate tasks in this way. \ No newline at end of file +> Note: it is currently not possible to start AWS Fargate tasks in this way. diff --git a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts index e4679e355d315..e33f647a2362a 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts @@ -6,14 +6,14 @@ import { singletonEventRole } from './util'; /** * Start a CodeBuild build when an AWS CloudWatch events rule is triggered. */ -export class CodeBuildProject implements events.IEventRuleTarget { +export class CodeBuildProject implements events.IRuleTarget { constructor(private readonly project: codebuild.IProject) { } /** * Allows using build projects as event rule targets. */ - public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(_rule: events.IRule): events.RuleTargetProperties { return { id: this.project.node.id, arn: this.project.projectArn, diff --git a/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts index 8e149a71e98b1..e17487c2e487c 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts @@ -6,11 +6,11 @@ import { singletonEventRole } from './util'; /** * Allows the pipeline to be used as a CloudWatch event rule target. */ -export class CodePipeline implements events.IEventRuleTarget { +export class CodePipeline implements events.IRuleTarget { constructor(private readonly pipeline: codepipeline.IPipeline) { } - public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(_rule: events.IRule): events.RuleTargetProperties { return { id: this.pipeline.node.id, arn: this.pipeline.pipelineArn, @@ -19,4 +19,4 @@ export class CodePipeline implements events.IEventRuleTarget { .addAction('codepipeline:StartPipelineExecution')]) }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts index 33003eea5e399..2fd38253fb8dd 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts @@ -57,7 +57,7 @@ export interface EcsEc2TaskProps { /** * Start a service on an EC2 cluster */ -export class EcsEc2Task implements events.IEventRuleTarget { +export class EcsEc2Task implements events.IRuleTarget { private readonly cluster: ecs.ICluster; private readonly taskDefinition: ecs.TaskDefinition; private readonly taskCount: number; @@ -75,7 +75,7 @@ export class EcsEc2Task implements events.IEventRuleTarget { /** * Allows using containers as target of CloudWatch events */ - public bind(rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(rule: events.IRule): events.RuleTargetProperties { const policyStatements = [new iam.PolicyStatement() .addAction('ecs:RunTask') .addResource(this.taskDefinition.taskDefinitionArn) @@ -100,9 +100,9 @@ export class EcsEc2Task implements events.IEventRuleTarget { taskCount: this.taskCount, taskDefinitionArn: this.taskDefinition.taskDefinitionArn }, - input: events.EventTargetInput.fromObject({ + input: events.RuleTargetInput.fromObject({ containerOverrides: this.props.containerOverrides, - networkConfiguration: this.renderNetworkConfiguration(rule as events.EventRule), + networkConfiguration: this.renderNetworkConfiguration(rule as events.Rule), }) }; } diff --git a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts index 6b9d4857bf7c3..d4e4b88d4fabc 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts @@ -13,13 +13,13 @@ export interface LambdaFunctionProps { * * @default the entire CloudWatch event */ - readonly event?: events.EventTargetInput; + readonly event?: events.RuleTargetInput; } /** * Use an AWS Lambda function as an event rule target. */ -export class LambdaFunction implements events.IEventRuleTarget { +export class LambdaFunction implements events.IRuleTarget { constructor(private readonly handler: lambda.IFunction, private readonly props: LambdaFunctionProps = {}) { } @@ -28,7 +28,7 @@ export class LambdaFunction implements events.IEventRuleTarget { * Returns a RuleTarget that can be used to trigger this Lambda as a * result from a CloudWatch event. */ - public bind(rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(rule: events.IRule): events.RuleTargetProperties { const permissionId = `AllowEventRule${rule.node.uniqueId}`; if (!this.handler.node.tryFindChild(permissionId)) { this.handler.addPermission(permissionId, { diff --git a/packages/@aws-cdk/aws-events-targets/lib/sns.ts b/packages/@aws-cdk/aws-events-targets/lib/sns.ts index c6a7118b91274..958e245e79abe 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sns.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sns.ts @@ -11,7 +11,7 @@ export interface SnsTopicProps { * * @default the entire CloudWatch event */ - readonly message?: events.EventTargetInput; + readonly message?: events.RuleTargetInput; } /** @@ -24,7 +24,7 @@ export interface SnsTopicProps { * repository.onCommit(new targets.SnsTopic(topic)); * */ -export class SnsTopic implements events.IEventRuleTarget { +export class SnsTopic implements events.IRuleTarget { constructor(public readonly topic: sns.ITopic, private readonly props: SnsTopicProps = {}) { } @@ -34,7 +34,7 @@ export class SnsTopic implements events.IEventRuleTarget { * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions */ - public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(_rule: events.IRule): events.RuleTargetProperties { // deduplicated automatically this.topic.grantPublish(new iam.ServicePrincipal('events.amazonaws.com')); diff --git a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts index 5fc31313a6425..10b1c27d3c4a5 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts @@ -12,22 +12,22 @@ export interface SfnStateMachineProps { * * @default the entire CloudWatch event */ - readonly input?: events.EventTargetInput; + readonly input?: events.RuleTargetInput; } /** * Use a StepFunctions state machine as a target for AWS CloudWatch event rules. */ -export class SfnStateMachine implements events.IEventRuleTarget { +export class SfnStateMachine implements events.IRuleTarget { constructor(public readonly machine: sfn.IStateMachine, private readonly props: SfnStateMachineProps = {}) { } /** - * Returns a properties that are used in an EventRule to trigger this State Machine + * Returns a properties that are used in an Rule to trigger this State Machine * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/resource-based-policies-cwe.html#sns-permissions */ - public bind(_rule: events.IEventRule): events.EventRuleTargetProperties { + public bind(_rule: events.IRule): events.RuleTargetProperties { return { id: this.machine.node.id, arn: this.machine.stateMachineArn, @@ -38,4 +38,4 @@ export class SfnStateMachine implements events.IEventRuleTarget { input: this.props.input }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index a16371beed75d..adc6bade85faa 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -8,7 +8,7 @@ test('use codebuild project as an eventrule target', () => { // GIVEN const stack = new Stack(); const project = new codebuild.Project(stack, 'MyProject', { source: new codebuild.CodePipelineSource() }); - const rule = new events.EventRule(stack, 'Rule', { scheduleExpression: 'rate(1 min)' }); + const rule = new events.Rule(stack, 'Rule', { scheduleExpression: 'rate(1 min)' }); // WHEN rule.addTarget(new targets.CodeBuildProject(project)); 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 aec1283fc9ffc..357a3c592ffe0 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 @@ -29,13 +29,13 @@ project.onStateChange('StateChange', new targets.SnsTopic(topic)); // The phase will be extracted from the "completed-phase" field of the event // details. project.onPhaseChange('PhaseChange').addTarget(new targets.SnsTopic(topic, { - message: events.EventTargetInput.fromText(`Build phase changed to ${codebuild.PhaseChangeEvent.completedPhase}`) + 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'); onCommitRule.addTarget(new targets.SnsTopic(topic, { - message: events.EventTargetInput.fromText( + 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/pipeline.test.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts index a990b69d95fef..38d642de3dbe0 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts @@ -31,7 +31,7 @@ test('use codebuild project as an eventrule target', () => { artifactBounds: { minInputs: 1, maxInputs: 1 , minOutputs: 1, maxOutputs: 1, }})] }); - const rule = new events.EventRule(stack, 'rule', { scheduleExpression: 'rate(1 min)' }); + const rule = new events.Rule(stack, 'rule', { scheduleExpression: 'rate(1 min)' }); // WHEN rule.addTarget(new targets.CodePipeline(pipeline)); @@ -78,4 +78,4 @@ class TestAction extends codepipeline.Action { protected bind(_info: codepipeline.ActionBind): void { // void } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts index 598dddc4188f2..3e9c62822ea55 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/ec2-event-rule-target.test.ts @@ -20,7 +20,7 @@ test("Can use EC2 taskdef as EventRule target", () => { memoryLimitMiB: 256 }); - const rule = new events.EventRule(stack, 'Rule', { + const rule = new events.Rule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)', }); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.ts index 150ae8033a0e1..a759421d8c047 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.ts @@ -30,12 +30,12 @@ class EventStack extends cdk.Stack { logging: new ecs.AwsLogDriver(this, 'TaskLogging', { streamPrefix: 'EventDemo' }) }); - // An EventRule that describes the event trigger (in this case a scheduled run) - const rule = new events.EventRule(this, 'Rule', { + // An Rule that describes the event trigger (in this case a scheduled run) + const rule = new events.Rule(this, 'Rule', { scheduleExpression: 'rate(1 minute)', }); - // Use Ec2TaskEventRuleTarget as the target of the EventRule + // Use EcsEc2Task as the target of the Rule rule.addTarget(new targets.EcsEc2Task({ cluster, taskDefinition, diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts index c815efec9a00f..1bde6cc2a0572 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts @@ -13,10 +13,10 @@ const fn = new lambda.Function(stack, 'MyFunc', { code: lambda.Code.inline(`exports.handler = ${handler.toString()}`) }); -const timer = new events.EventRule(stack, 'Timer', { scheduleExpression: 'rate(1 minute)' }); +const timer = new events.Rule(stack, 'Timer', { scheduleExpression: 'rate(1 minute)' }); timer.addTarget(new targets.LambdaFunction(fn)); -const timer2 = new events.EventRule(stack, 'Timer2', { scheduleExpression: 'rate(2 minutes)' }); +const timer2 = new events.Rule(stack, 'Timer2', { scheduleExpression: 'rate(2 minutes)' }); timer2.addTarget(new targets.LambdaFunction(fn)); app.run(); diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts index e611681c5d648..edef10526eef6 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts @@ -8,8 +8,8 @@ test('use lambda as an event rule target', () => { // GIVEN const stack = new cdk.Stack(); const fn = newTestLambda(stack); - const rule1 = new events.EventRule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); - const rule2 = new events.EventRule(stack, 'Rule2', { scheduleExpression: 'rate(5 minutes)' }); + const rule1 = new events.Rule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); + const rule2 = new events.Rule(stack, 'Rule2', { scheduleExpression: 'rate(5 minutes)' }); // WHEN rule1.addTarget(new targets.LambdaFunction(fn)); diff --git a/packages/@aws-cdk/aws-events-targets/test/sns/integ.sns-event-rule-target.ts b/packages/@aws-cdk/aws-events-targets/test/sns/integ.sns-event-rule-target.ts index 6a07c8379ffa0..81fe6e95c5a48 100644 --- a/packages/@aws-cdk/aws-events-targets/test/sns/integ.sns-event-rule-target.ts +++ b/packages/@aws-cdk/aws-events-targets/test/sns/integ.sns-event-rule-target.ts @@ -14,7 +14,7 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-sns-event-target'); const topic = new sns.Topic(stack, 'MyTopic'); -const event = new events.EventRule(stack, 'EveryMinute', { +const event = new events.Rule(stack, 'EveryMinute', { scheduleExpression: 'rate(1 minute)' }); diff --git a/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts b/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts index 6c03182824b17..868255b475e55 100644 --- a/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/sns/sns.test.ts @@ -8,7 +8,7 @@ test('sns topic as an event rule target', () => { // GIVEN const stack = new Stack(); const topic = new sns.Topic(stack, 'MyTopic'); - const rule = new events.EventRule(stack, 'MyRule', { + const rule = new events.Rule(stack, 'MyRule', { scheduleExpression: 'rate(1 hour)', }); @@ -51,7 +51,7 @@ test('multiple uses of a topic as a target results in a single policy statement' // WHEN for (let i = 0; i < 5; ++i) { - const rule = new events.EventRule(stack, `Rule${i}`, { scheduleExpression: 'rate(1 hour)' }); + const rule = new events.Rule(stack, `Rule${i}`, { scheduleExpression: 'rate(1 hour)' }); rule.addTarget(new targets.SnsTopic(topic)); } diff --git a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts index 4225a0129fde4..5e29cd47b046c 100644 --- a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts @@ -7,7 +7,7 @@ import targets = require('../../lib'); test('State machine can be used as Event Rule target', () => { // GIVEN const stack = new cdk.Stack(); - const rule = new events.EventRule(stack, 'Rule', { + const rule = new events.Rule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); const stateMachine = new sfn.StateMachine(stack, 'SM', { @@ -16,7 +16,7 @@ test('State machine can be used as Event Rule target', () => { // WHEN rule.addTarget(new targets.SfnStateMachine(stateMachine, { - input: events.EventTargetInput.fromObject({ SomeParam: 'SomeValue' }), + input: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }), })); // THEN diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index e89bfdce98166..9ce481c191009 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -28,14 +28,14 @@ event when the pipeline changes it's state. that are of interest to them. A rule can customize the JSON sent to the target, by passing only certain parts or by overwriting it with a constant. -The `EventRule` construct defines a CloudWatch events rule which monitors an +The `Rule` construct defines a CloudWatch events rule which monitors an event based on an [event pattern](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html) and invoke __event targets__ when the pattern is matched against a triggered event. Event targets are objects that implement the `IEventTarget` interface. Normally, you will use one of the `source.onXxx(name[, target[, options]]) -> -EventRule` methods on the event source to define an event rule associated with +Rule` methods on the event source to define an event rule associated with the specific activity. You can targets either via props, or add targets using `rule.addTarget`. @@ -65,7 +65,7 @@ onCommitRule.addTarget(topic, { ## Event Targets -The `@aws-cdk/aws-events-targets` module includes classes that implement the `IEventRuleTarget` +The `@aws-cdk/aws-events-targets` module includes classes that implement the `IRuleTarget` interface for various AWS services. The following targets are supported: diff --git a/packages/@aws-cdk/aws-events/lib/input.ts b/packages/@aws-cdk/aws-events/lib/input.ts index ec70050800024..b3ed4dddb7020 100644 --- a/packages/@aws-cdk/aws-events/lib/input.ts +++ b/packages/@aws-cdk/aws-events/lib/input.ts @@ -1,17 +1,17 @@ import { CloudFormationLang, DefaultTokenResolver, IResolveContext, resolve, StringConcat, Token } from '@aws-cdk/cdk'; -import { IEventRule } from './rule-ref'; +import { IRule } from './rule-ref'; /** * The input to send to the event target */ -export abstract class EventTargetInput { +export abstract class RuleTargetInput { /** * Pass text to the event target * * May contain strings returned by EventField.from() to substitute in parts of the * matched event. */ - public static fromText(text: string): EventTargetInput { + public static fromText(text: string): RuleTargetInput { return new FieldAwareEventInput(text, InputType.Text); } @@ -24,7 +24,7 @@ export abstract class EventTargetInput { * May contain strings returned by EventField.from() to substitute in parts * of the matched event. */ - public static fromMultilineText(text: string): EventTargetInput { + public static fromMultilineText(text: string): RuleTargetInput { return new FieldAwareEventInput(text, InputType.Multiline); } @@ -34,14 +34,14 @@ export abstract class EventTargetInput { * May contain strings returned by EventField.from() to substitute in parts of the * matched event. */ - public static fromObject(obj: any): EventTargetInput { + public static fromObject(obj: any): RuleTargetInput { return new FieldAwareEventInput(obj, InputType.Object); } /** * Take the event target input from a path in the event JSON */ - public static fromEventPath(path: string): EventTargetInput { + public static fromEventPath(path: string): RuleTargetInput { return new LiteralEventInput({ inputPath: path }); } @@ -51,13 +51,13 @@ export abstract class EventTargetInput { /** * Return the input properties for this input object */ - public abstract bind(rule: IEventRule): EventRuleTargetInputProperties; + public abstract bind(rule: IRule): RuleTargetInputProperties; } /** * The input properties for an event target */ -export interface EventRuleTargetInputProperties { +export interface RuleTargetInputProperties { /** * Literal input to the target service (must be valid JSON) */ @@ -82,15 +82,15 @@ export interface EventRuleTargetInputProperties { /** * Event Input that is directly derived from the construct */ -class LiteralEventInput extends EventTargetInput { - constructor(private readonly props: EventRuleTargetInputProperties) { +class LiteralEventInput extends RuleTargetInput { + constructor(private readonly props: RuleTargetInputProperties) { super(); } /** * Return the input properties for this input object */ - public bind(_rule: IEventRule): EventRuleTargetInputProperties { + public bind(_rule: IRule): RuleTargetInputProperties { return this.props; } } @@ -119,12 +119,12 @@ class LiteralEventInput extends EventTargetInput { * To achieve the latter, we postprocess the JSON string to remove the surrounding * quotes by using a string replace. */ -class FieldAwareEventInput extends EventTargetInput { +class FieldAwareEventInput extends RuleTargetInput { constructor(private readonly input: any, private readonly inputType: InputType) { super(); } - public bind(rule: IEventRule): EventRuleTargetInputProperties { + public bind(rule: IRule): RuleTargetInputProperties { let fieldCounter = 0; const pathToKey = new Map(); const inputPathsMap: {[key: string]: string} = {}; @@ -294,4 +294,4 @@ const EVENT_FIELD_SYMBOL = Symbol.for('@aws-cdk/aws-events.EventField'); */ function regexQuote(s: string) { return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events/lib/rule-ref.ts b/packages/@aws-cdk/aws-events/lib/rule-ref.ts index 8a262b8c6ecaa..52455797ae23d 100644 --- a/packages/@aws-cdk/aws-events/lib/rule-ref.ts +++ b/packages/@aws-cdk/aws-events/lib/rule-ref.ts @@ -1,6 +1,6 @@ import { IResource } from '@aws-cdk/cdk'; -export interface IEventRule extends IResource { +export interface IRule extends IResource { /** * The value of the event rule Amazon Resource Name (ARN), such as * arn:aws:events:us-east-2:123456789012:rule/example. diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index d5c01804b1898..b70e6cdfa2453 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,11 +1,11 @@ import { Construct, Resource, Token } from '@aws-cdk/cdk'; import { EventPattern } from './event-pattern'; import { CfnRule } from './events.generated'; -import { IEventRule } from './rule-ref'; -import { IEventRuleTarget } from './target'; +import { IRule } from './rule-ref'; +import { IRuleTarget } from './target'; import { mergeEventPattern } from './util'; -export interface EventRuleProps { +export interface RuleProps { /** * A description of the rule's purpose. */ @@ -56,7 +56,7 @@ export interface EventRuleProps { * Input will be the full matched event. If you wish to specify custom * target input, use `addTarget(target[, inputOptions])`. */ - readonly targets?: IEventRuleTarget[]; + readonly targets?: IRuleTarget[]; } /** @@ -64,10 +64,10 @@ export interface EventRuleProps { * * @resource AWS::Events::Rule */ -export class EventRule extends Resource implements IEventRule { +export class Rule extends Resource implements IRule { - public static fromEventRuleArn(scope: Construct, id: string, eventRuleArn: string): IEventRule { - class Import extends Resource implements IEventRule { + public static fromEventRuleArn(scope: Construct, id: string, eventRuleArn: string): IRule { + class Import extends Resource implements IRule { public ruleArn = eventRuleArn; } return new Import(scope, id); @@ -79,7 +79,7 @@ export class EventRule extends Resource implements IEventRule { private readonly eventPattern: EventPattern = { }; private scheduleExpression?: string; - constructor(scope: Construct, id: string, props: EventRuleProps = { }) { + constructor(scope: Construct, id: string, props: RuleProps = { }) { super(scope, id); const resource = new CfnRule(this, 'Resource', { @@ -107,7 +107,7 @@ export class EventRule extends Resource implements IEventRule { * * No-op if target is undefined. */ - public addTarget(target?: IEventRuleTarget) { + public addTarget(target?: IRuleTarget) { if (!target) { return; } const targetProps = target.bind(this); diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index 18ad6ddf8d205..4c77425fc7865 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -1,25 +1,25 @@ import iam = require('@aws-cdk/aws-iam'); import { CfnRule } from './events.generated'; -import { EventTargetInput } from './input'; -import { IEventRule } from './rule-ref'; +import { RuleTargetInput } from './input'; +import { IRule } from './rule-ref'; /** * An abstract target for EventRules. */ -export interface IEventRuleTarget { +export interface IRuleTarget { /** * Returns the rule target specification. * NOTE: Do not use the various `inputXxx` options. They can be set in a call to `addTarget`. * * @param rule The CloudWatch Event Rule that would trigger this target. */ - bind(rule: IEventRule): EventRuleTargetProperties; + bind(rule: IRule): RuleTargetProperties; } /** * Properties for an event rule target */ -export interface EventRuleTargetProperties { +export interface RuleTargetProperties { /** * A unique, user-defined identifier for the target. Acceptable values * include alphanumeric characters, periods (.), hyphens (-), and @@ -61,5 +61,5 @@ export interface EventRuleTargetProperties { * * @default the entire event */ - readonly input?: EventTargetInput; + readonly input?: RuleTargetInput; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index cb6df48a95b88..710cad820a06f 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -75,5 +75,11 @@ }, "engines": { "node": ">= 8.10.0" + }, + "awslint": { + "exclude": [ + "from-method:@aws-cdk/aws-events.Rule" + ] } + } diff --git a/packages/@aws-cdk/aws-events/test/test.input.ts b/packages/@aws-cdk/aws-events/test/test.input.ts index e629a88ad4333..8ca4b6659d787 100644 --- a/packages/@aws-cdk/aws-events/test/test.input.ts +++ b/packages/@aws-cdk/aws-events/test/test.input.ts @@ -2,20 +2,20 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { EventTargetInput, IEventRuleTarget } from '../lib'; -import { EventRule } from '../lib/rule'; +import { IRuleTarget, RuleTargetInput } from '../lib'; +import { Rule } from '../lib/rule'; export = { 'json template': { 'can just be a JSON object'(test: Test) { // GIVEN const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { + const rule = new Rule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); // WHEN - rule.addTarget(new SomeTarget(EventTargetInput.fromObject({ SomeObject: 'withAValue' }))); + rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ SomeObject: 'withAValue' }))); // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { @@ -33,12 +33,12 @@ export = { 'strings with newlines are serialized to a newline-delimited list of JSON strings'(test: Test) { // GIVEN const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { + const rule = new Rule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); // WHEN - rule.addTarget(new SomeTarget(EventTargetInput.fromMultilineText('I have\nmultiple lines'))); + rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('I have\nmultiple lines'))); // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { @@ -55,12 +55,12 @@ export = { 'escaped newlines are not interpreted as newlines'(test: Test) { // GIVEN const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { + const rule = new Rule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); // WHEN - rule.addTarget(new SomeTarget(EventTargetInput.fromMultilineText('this is not\\na real newline'))), + rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('this is not\\na real newline'))), // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { @@ -77,14 +77,14 @@ export = { 'can use Tokens in text templates'(test: Test) { // GIVEN const stack = new Stack(); - const rule = new EventRule(stack, 'Rule', { + const rule = new Rule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); const world = new cdk.Token(() => 'world'); // WHEN - rule.addTarget(new SomeTarget(EventTargetInput.fromText(`hello ${world}`))); + rule.addTarget(new SomeTarget(RuleTargetInput.fromText(`hello ${world}`))); // THEN expect(stack).to(haveResourceLike('AWS::Events::Rule', { @@ -100,8 +100,8 @@ export = { }, }; -class SomeTarget implements IEventRuleTarget { - public constructor(private readonly input: EventTargetInput) { +class SomeTarget implements IRuleTarget { + public constructor(private readonly input: RuleTargetInput) { } public bind() { diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index b0c6d25e7bb19..352bb814d98c5 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -4,8 +4,8 @@ import { ServicePrincipal } from '@aws-cdk/aws-iam'; import cdk = require('@aws-cdk/cdk'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { EventField, EventTargetInput, IEventRule, IEventRuleTarget } from '../lib'; -import { EventRule } from '../lib/rule'; +import { EventField, IRule, IRuleTarget, RuleTargetInput } from '../lib'; +import { Rule } from '../lib/rule'; // tslint:disable:object-literal-key-quotes @@ -13,7 +13,7 @@ export = { 'default rule'(test: Test) { const stack = new cdk.Stack(); - new EventRule(stack, 'MyRule', { + new Rule(stack, 'MyRule', { scheduleExpression: 'rate(10 minutes)' }); @@ -36,7 +36,7 @@ export = { const stack = new cdk.Stack(); // WHEN - new EventRule(stack, 'MyRule', { + new Rule(stack, 'MyRule', { ruleName: 'PhysicalName', scheduleExpression: 'rate(10 minutes)' }); @@ -52,7 +52,7 @@ export = { 'eventPattern is rendered properly'(test: Test) { const stack = new cdk.Stack(); - new EventRule(stack, 'MyRule', { + new Rule(stack, 'MyRule', { eventPattern: { account: [ 'account1', 'account2' ], detail: { @@ -96,7 +96,7 @@ export = { 'fails synthesis if neither eventPattern nor scheudleExpression are specified'(test: Test) { const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyStack'); - new EventRule(stack, 'Rule'); + new Rule(stack, 'Rule'); test.throws(() => app.synthesizeStack(stack.name), /Either 'eventPattern' or 'scheduleExpression' must be defined/); test.done(); }, @@ -104,7 +104,7 @@ export = { 'addEventPattern can be used to add filters'(test: Test) { const stack = new cdk.Stack(); - const rule = new EventRule(stack, 'MyRule'); + const rule = new Rule(stack, 'MyRule'); rule.addEventPattern({ account: [ '12345' ], detail: { @@ -156,7 +156,7 @@ export = { 'targets can be added via props or addTarget with input transformer'(test: Test) { const stack = new cdk.Stack(); - const t1: IEventRuleTarget = { + const t1: IRuleTarget = { bind: () => ({ id: 'T1', arn: 'ARN1', @@ -164,15 +164,15 @@ export = { }) }; - const t2: IEventRuleTarget = { + const t2: IRuleTarget = { bind: () => ({ id: 'T2', arn: 'ARN2', - input: EventTargetInput.fromText(`This is ${EventField.fromPath('$.detail.bla', 'bla')}`), + input: RuleTargetInput.fromText(`This is ${EventField.fromPath('$.detail.bla', 'bla')}`), }) }; - const rule = new EventRule(stack, 'EventRule', { + const rule = new Rule(stack, 'EventRule', { targets: [ t1 ], scheduleExpression: 'rate(5 minutes)' }); @@ -215,12 +215,12 @@ export = { 'input template can contain tokens'(test: Test) { const stack = new cdk.Stack(); - const rule = new EventRule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); + const rule = new Rule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); // a plain string should just be stringified (i.e. double quotes added and escaped) rule.addTarget({ bind: () => ({ - id: 'T2', arn: 'ARN2', input: EventTargetInput.fromText('Hello, "world"') + id: 'T2', arn: 'ARN2', input: RuleTargetInput.fromText('Hello, "world"') }) }); @@ -229,7 +229,7 @@ export = { rule.addTarget({ bind: () => ({ id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' }, - input: EventTargetInput.fromText(cdk.Fn.join('', [ 'a', 'b' ]).toString()), + input: RuleTargetInput.fromText(cdk.Fn.join('', [ 'a', 'b' ]).toString()), }) }); @@ -237,7 +237,7 @@ export = { rule.addTarget({ bind: () => ({ id: 'T3', arn: 'ARN3', - input: EventTargetInput.fromObject({ foo: EventField.fromPath('$.detail.bar') }), + input: RuleTargetInput.fromObject({ foo: EventField.fromPath('$.detail.bar') }), }) }); @@ -245,7 +245,7 @@ export = { rule.addTarget({ bind: () => ({ id: 'T4', arn: 'ARN4', - input: EventTargetInput.fromText(cdk.Fn.join(' ', ['hello', '"world"']).toString()), + input: RuleTargetInput.fromText(cdk.Fn.join(' ', ['hello', '"world"']).toString()), }) }); @@ -298,7 +298,7 @@ export = { // GIVEN const stack = new cdk.Stack(); - const rule = new EventRule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); + const rule = new Rule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); const role = new iam.Role(stack, 'SomeRole', { assumedBy: new ServicePrincipal('nobody') @@ -333,8 +333,8 @@ export = { let receivedRuleArn = 'FAIL'; let receivedRuleId = 'FAIL'; - const t1: IEventRuleTarget = { - bind: (eventRule: IEventRule) => { + const t1: IRuleTarget = { + bind: (eventRule: IRule) => { receivedRuleArn = eventRule.ruleArn; receivedRuleId = eventRule.node.uniqueId; @@ -346,7 +346,7 @@ export = { } }; - const rule = new EventRule(stack, 'EventRule'); + const rule = new Rule(stack, 'EventRule'); rule.addTarget(t1); test.deepEqual(stack.node.resolve(receivedRuleArn), stack.node.resolve(rule.ruleArn)); @@ -359,7 +359,7 @@ export = { const stack = new Stack(); // WHEN - const importedRule = EventRule.fromEventRuleArn(stack, 'ImportedRule', 'arn:of:rule'); + const importedRule = Rule.fromEventRuleArn(stack, 'ImportedRule', 'arn:of:rule'); // THEN test.deepEqual(importedRule.ruleArn, 'arn:of:rule'); @@ -371,7 +371,7 @@ export = { const stack = new cdk.Stack(); // WHEN - new EventRule(stack, 'Rule', { + new Rule(stack, 'Rule', { scheduleExpression: 'foom', enabled: false }); @@ -387,7 +387,7 @@ export = { 'fails if multiple targets with the same id are added'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const rule = new EventRule(stack, 'Rule', { + const rule = new Rule(stack, 'Rule', { scheduleExpression: 'foom', enabled: false }); @@ -399,7 +399,7 @@ export = { } }; -class SomeTarget implements IEventRuleTarget { +class SomeTarget implements IRuleTarget { public bind() { return { id: 'T1', arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' } diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 0fca2c2e13a64..1774c650827df 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -176,9 +176,9 @@ export interface IBucket extends IResource { * @param name the logical ID of the newly created Event Rule * @param target the optional target of the Event Rule * @param path the optional path inside the Bucket that will be watched for changes - * @returns a new {@link events.EventRule} instance + * @returns a new {@link events.Rule} instance */ - onPutObject(name: string, target?: events.IEventRuleTarget, path?: string): events.EventRule; + onPutObject(name: string, target?: events.IRuleTarget, path?: string): events.Rule; } /** @@ -283,8 +283,8 @@ abstract class BucketBase extends Resource implements IBucket { */ protected abstract disallowPublicAccess?: boolean; - public onPutObject(name: string, target?: events.IEventRuleTarget, path?: string): events.EventRule { - const eventRule = new events.EventRule(this, name, { + public onPutObject(name: string, target?: events.IRuleTarget, path?: string): events.Rule { + const eventRule = new events.Rule(this, name, { eventPattern: { source: [ 'aws.s3', From 80f020e1338606bd896c8306f9948f0313a7c426 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 May 2019 15:07:13 +0200 Subject: [PATCH 33/33] Fix integ test --- .../test/ecs/integ.event-task.lit.expected.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json index 8fc7250111940..ed1654619ee50 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json @@ -1052,7 +1052,7 @@ "Input": "{\"containerOverrides\":[{\"containerName\":\"TheContainer\",\"environment\":[{\"name\":\"I_WAS_TRIGGERED\",\"value\":\"From CloudWatch Events\"}]}]}", "RoleArn": { "Fn::GetAtt": [ - "awsecsintegecsTaskDef8DD0C801EventsRoleC617AC5B", + "TaskDefEventsRoleFB3B67B8", "Arn" ] }