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 24, 2019
1 parent c989fa4 commit 8a5402a
Show file tree
Hide file tree
Showing 10 changed files with 292 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,
};
}
}
109 changes: 101 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, Token } 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,109 @@ 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 targetAccount = targetStack.account;
const targetRegion = targetStack.region;

const sourceStack = Stack.of(this);
const sourceAccount = sourceStack.account;

if (targetRegion !== sourceStack.region) {
throw new Error('Rule and target must be in the same region');
}

if (targetAccount !== 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

// for cross-account events, we require concrete accounts
if (Token.isUnresolved(targetAccount)) {
throw new Error('You need to provide a concrete account for the target stack when using cross-account events');
}
if (Token.isUnresolved(sourceAccount)) {
throw new Error('You need to provide a concrete account for the source stack when using cross-account events');
}
// and the target region has to be concrete as well
if (Token.isUnresolved(targetRegion)) {
throw new Error('You need to provide a concrete region for the target stack when using cross-account events');
}

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

// 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}-${targetRegion}-${targetAccount}`;
let eventBusPolicyStack: Stack = app.node.tryFindChild(stackId) as Stack;
if (!eventBusPolicyStack) {
eventBusPolicyStack = new Stack(app, stackId, {
env: {
account: targetAccount,
region: targetRegion,
},
stackName: `${targetStack.stackName}-EventBusPolicy-support-${targetRegion}-${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;
}
Loading

0 comments on commit 8a5402a

Please sign in to comment.