Skip to content

Commit

Permalink
feat(events): ability to add cross-account targets
Browse files Browse the repository at this point in the history
  • Loading branch information
skinny85 committed Jul 20, 2019
1 parent 5879178 commit 856f451
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 17 deletions.
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events-targets/lib/codebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class CodeBuildProject implements events.IRuleTarget {
actions: ['codebuild:StartBuild'],
resources: [this.project.projectArn],
})]),
targetResource: this.project,
};
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export class CodePipeline implements events.IRuleTarget {
role: singletonEventRole(this.pipeline, [new iam.PolicyStatement({
resources: [this.pipeline.pipelineArn],
actions: ['codepipeline:StartPipelineExecution'],
})])
})]),
targetResource: this.pipeline,
};
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ export class EcsTask implements events.IRuleTarget {
taskCount,
taskDefinitionArn
},
input: events.RuleTargetInput.fromObject(input)
input: events.RuleTargetInput.fromObject(input),
targetResource: this.taskDefinition,
};
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events-targets/lib/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class LambdaFunction implements events.IRuleTarget {
id: '',
arn: this.handler.functionArn,
input: this.props.event,
targetResource: this.handler,
};
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events-targets/lib/sns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class SnsTopic implements events.IRuleTarget {
id: '',
arn: this.topic.topicArn,
input: this.props.message,
targetResource: this.topic,
};
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/sqs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ export class SqsQueue implements events.IRuleTarget {
})
);

const result = {
const result: events.RuleTargetConfig = {
id: '',
arn: this.queue.queueArn,
input: this.props.message,
targetResource: this.queue,
};
if (!!this.props.messageGroupId) {
Object.assign(result, { sqsParameters: { messageGroupId: this.props.messageGroupId } });
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/state-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export class SfnStateMachine implements events.IRuleTarget {
actions: ['states:StartExecution'],
resources: [this.machine.stateMachineArn]
})]),
input: this.props.input
input: this.props.input,
targetResource: this.machine,
};
}
}
88 changes: 80 additions & 8 deletions packages/@aws-cdk/aws-events/lib/rule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct, Lazy, Resource } from '@aws-cdk/core';
import { App, Construct, Lazy, Resource, Stack } from '@aws-cdk/core';
import { EventPattern } from './event-pattern';
import { CfnRule } from './events.generated';
import { CfnEventBusPolicy, CfnRule } from './events.generated';
import { IRule } from './rule-ref';
import { Schedule } from './schedule';
import { IRuleTarget } from './target';
Expand Down Expand Up @@ -88,16 +88,19 @@ export class Rule extends Resource implements IRule {

private readonly targets = new Array<CfnRule.TargetProperty>();
private readonly eventPattern: EventPattern = { };
private scheduleExpression?: string;
private readonly scheduleExpression?: string;
private readonly description?: string;
private readonly accountEventBusTargets: { [account: string]: boolean } = {};

constructor(scope: Construct, id: string, props: RuleProps = { }) {
super(scope, id, {
physicalName: props.ruleName,
});
this.description = props.description;

const resource = new CfnRule(this, 'Resource', {
name: this.physicalName,
description: props.description,
description: this.description,
state: props.enabled == null ? 'ENABLED' : (props.enabled ? 'ENABLED' : 'DISABLED'),
scheduleExpression: Lazy.stringValue({ produce: () => this.scheduleExpression }),
eventPattern: Lazy.anyValue({ produce: () => this.renderEventPattern() }),
Expand All @@ -124,19 +127,88 @@ export class Rule extends Resource implements IRule {
*
* No-op if target is undefined.
*/
public addTarget(target?: IRuleTarget) {
public addTarget(target?: IRuleTarget): void {
if (!target) { return; }

// Simply increment id for each `addTarget` call. This is guaranteed to be unique.
const id = `Target${this.targets.length}`;
const autoGeneratedId = `Target${this.targets.length}`;

const targetProps = target.bind(this, id);
const targetProps = target.bind(this, autoGeneratedId);
const inputProps = targetProps.input && targetProps.input.bind(this);

const roleArn = targetProps.role ? targetProps.role.roleArn : undefined;
const id = targetProps.id || autoGeneratedId;

if (targetProps.targetResource) {
const targetStack = Stack.of(targetProps.targetResource);
const sourceStack = Stack.of(this);
const sourceAccount = sourceStack.account;
if (targetStack.account !== sourceAccount) {
// cross-account event - strap in, this works differently than regular events!
// based on:
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEvents-CrossAccountEventDelivery.html

// the _actual_ target is just the event bus of the target's account
// make sure we only add it once
const exists = this.accountEventBusTargets[targetStack.account];
if (!exists) {
this.accountEventBusTargets[targetStack.account] = true;
this.targets.push({
id,
arn: targetStack.formatArn({
service: 'events',
resource: 'event-bus',
resourceName: 'default',
region: targetStack.region,
account: targetStack.account,
}),
});
}

// Grant the source account permissions to publish events to the event bus of the target account.
// Do it in a separate stack instead of the target stack (which seems like the obvious place to put it),
// because it needs to be deployed before the rule containing the above event-bus target in the source stack
// (CloudWatch verifies whether you have permissions to the targets on rule creation),
// but it's common for the target stack to depend on the source stack
// (that's the case with CodePipeline, for example)
const app = this.node.root;
if (!app || !App.isApp(app)) {
throw new Error(`Event stack which uses cross-account targets must be part of a CDK app`);
}
const stackId = `EventBusPolicy-${sourceAccount}-${targetStack.account}`;
let eventBusPolicyStack: Stack = app.node.tryFindChild(stackId) as Stack;
if (!eventBusPolicyStack) {
eventBusPolicyStack = new Stack(app, stackId, {
env: {
account: targetStack.account,
region: targetStack.region,
},
stackName: `${targetStack.stackName}-EventBusPolicy-support-${sourceAccount}`,
});
new CfnEventBusPolicy(eventBusPolicyStack, `GivePermToOtherAccount`, {
action: 'events:PutEvents',
statementId: 'MySid',
principal: sourceAccount,
});
}
// deploy this before the source stack
sourceStack.addDependency(eventBusPolicyStack);

// The actual rule lives in the target stack.
// Other than the account, it's identical to this one
new Rule(targetStack, `${this.node.uniqueId}-${id}`, {
targets: [target],
eventPattern: this.eventPattern,
schedule: this.scheduleExpression ? Schedule.expression(this.scheduleExpression) : undefined,
description: this.description,
});

return;
}
}

this.targets.push({
id: targetProps.id || id,
id,
arn: targetProps.arn,
roleArn,
ecsParameters: targetProps.ecsParameters,
Expand Down
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-events/lib/target.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import iam = require('@aws-cdk/aws-iam');
import { IConstruct } from '@aws-cdk/core';
import { CfnRule } from './events.generated';
import { RuleTargetInput } from './input';
import { IRule } from './rule-ref';
Expand Down Expand Up @@ -65,4 +66,12 @@ export interface RuleTargetConfig {
* @default the entire event
*/
readonly input?: RuleTargetInput;

/**
* The construct that is backing this target.
* This is the resource that will actually have some action performed on it when used as a target
* (for example, start a build for a CodeBuild project).
* We need it to determine whether the
*/
readonly targetResource?: IConstruct;
}
110 changes: 105 additions & 5 deletions packages/@aws-cdk/aws-events/test/test.rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ServicePrincipal } from '@aws-cdk/aws-iam';
import cdk = require('@aws-cdk/core');
import { Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
import { EventField, IRule, IRuleTarget, RuleTargetInput, Schedule } from '../lib';
import { EventField, IRule, IRuleTarget, RuleTargetConfig, RuleTargetInput, Schedule } from '../lib';
import { Rule } from '../lib/rule';

// tslint:disable:object-literal-key-quotes
Expand Down Expand Up @@ -422,14 +422,114 @@ export = {
}));

test.done();
}
},

'works when providing targets from different accounts'(test: Test) {
const app = new cdk.App();

const sourceAccount = '123456789012';
const sourceStack = new cdk.Stack(app, 'SourceStack', {
env: {
account: sourceAccount,
},
});
const rule = new Rule(sourceStack, 'Rule', {
eventPattern: {
source: ['some-event'],
},
});

const targetAccount = '234567890123';
const targetStack = new cdk.Stack(app, 'TargetStack', {
env: {
account: targetAccount,
},
});
const resource1 = new iam.Role(targetStack, 'Resource1', {
assumedBy: new iam.AnyPrincipal(),
});
const resource2 = new iam.Role(targetStack, 'Resource2', {
assumedBy: new iam.AnyPrincipal(),
});

rule.addTarget(new SomeTarget('T1', resource1));
rule.addTarget(new SomeTarget('T2', resource2));

expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', {
"EventPattern": {
"source": [
"some-event",
],
},
"State": "ENABLED",
"Targets": [
{
"Id": "T1",
"Arn": {
"Fn::Join": [
"",
[
"arn:",
{ "Ref": "AWS::Partition" },
":events:",
{ "Ref": "AWS::Region" },
`:${targetAccount}:event-bus/default`,
],
],
},
},
],
}));

expect(targetStack).to(haveResourceLike('AWS::Events::Rule', {
"EventPattern": {
"source": [
"some-event",
],
},
"State": "ENABLED",
"Targets": [
{
"Id": "T1",
"Arn": "ARN1",
},
],
}));
expect(targetStack).to(haveResourceLike('AWS::Events::Rule', {
"EventPattern": {
"source": [
"some-event",
],
},
"State": "ENABLED",
"Targets": [
{
"Id": "T2",
"Arn": "ARN1",
},
],
}));

const eventBusPolicyStack = app.node.findChild(`EventBusPolicy-${sourceAccount}-${targetAccount}`) as cdk.Stack;
expect(eventBusPolicyStack).to(haveResourceLike('AWS::Events::EventBusPolicy', {
"Action": "events:PutEvents",
"StatementId": "MySid",
"Principal": sourceAccount,
}));

test.done();
},
};

class SomeTarget implements IRuleTarget {
public bind() {
public constructor(private readonly id?: string, private readonly resource?: cdk.IConstruct) {
}

public bind(): RuleTargetConfig {
return {
id: '',
arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' }
id: this.id || '',
arn: 'ARN1', kinesisParameters: { partitionKeyPath: 'partitionKeyPath' },
targetResource: this.resource,
};
}
}

0 comments on commit 856f451

Please sign in to comment.