-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
- Rename SubscriptionDestionation->LogSubscriptionDestination. - Introducing Arn.parseToken() - Use FnConcat instead of FnSub to build a region-aware service principal - Add an additional safeguard in the cross-account subscription generation for if one account is unset. Don't forget to mention this fixes #174.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { Test } from 'nodeunit'; | ||
import { Arn, ArnComponents, resolve } from '../../lib'; | ||
import { Arn, ArnComponents, resolve, Token } from '../../lib'; | ||
|
||
export = { | ||
'create from components with defaults'(test: Test) { | ||
|
@@ -187,7 +187,36 @@ export = { | |
}); | ||
|
||
test.done(); | ||
} | ||
}, | ||
|
||
'a Token with : separator'(test: Test) { | ||
const theToken = { Ref: 'SomeParameter' }; | ||
const parsed = Arn.parseToken(new Token(() => theToken), ':'); | ||
|
||
test.deepEqual(resolve(parsed.partition), { 'Fn::Select': [ 1, { 'Fn::Split': [ ':', theToken ]} ]}); | ||
test.deepEqual(resolve(parsed.service), { 'Fn::Select': [ 2, { 'Fn::Split': [ ':', theToken ]} ]}); | ||
test.deepEqual(resolve(parsed.region), { 'Fn::Select': [ 3, { 'Fn::Split': [ ':', theToken ]} ]}); | ||
test.deepEqual(resolve(parsed.account), { 'Fn::Select': [ 4, { 'Fn::Split': [ ':', theToken ]} ]}); | ||
test.deepEqual(resolve(parsed.resource), { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]}); | ||
test.deepEqual(resolve(parsed.resourceName), { 'Fn::Select': [ 6, { 'Fn::Split': [ ':', theToken ]} ]}); | ||
test.equal(parsed.sep, ':'); | ||
|
||
test.done(); | ||
}, | ||
|
||
'a Token with / separator'(test: Test) { | ||
const theToken = { Ref: 'SomeParameter' }; | ||
const parsed = Arn.parseToken(new Token(() => theToken)); | ||
|
||
test.equal(parsed.sep, '/'); | ||
|
||
// tslint:disable-next-line:max-line-length | ||
test.deepEqual(resolve(parsed.resource), { 'Fn::Select': [ 0, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); | ||
// tslint:disable-next-line:max-line-length | ||
test.deepEqual(resolve(parsed.resourceName), { 'Fn::Select': [ 1, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); | ||
|
||
test.done(); | ||
} | ||
}, | ||
}; | ||
|
||
}; | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
rix0rrr
Contributor
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import { Construct, FnConcat, FnSelect, FnSplit, FnSub, Output, PolicyStatement, ServicePrincipal, Stack, Token } from '@aws-cdk/core'; | ||
import { Arn, AwsRegion, Construct, FnConcat, HashedAddressingScheme, Output, | ||
PolicyStatement, ServicePrincipal, Stack, Token } from '@aws-cdk/core'; | ||
import { IIdentityResource, Role } from '@aws-cdk/iam'; | ||
import * as kms from '@aws-cdk/kms'; | ||
import logs = require('@aws-cdk/logs'); | ||
|
@@ -38,7 +39,7 @@ export interface StreamRefProps { | |
* StreamRef.import(this, 'MyImportedStream', ref); | ||
* | ||
*/ | ||
export abstract class StreamRef extends Construct implements logs.ISubscriptionDestination { | ||
export abstract class StreamRef extends Construct implements logs.ILogSubscriptionDestination { | ||
/** | ||
* Creates a Stream construct that represents an external stream. | ||
* | ||
|
@@ -170,12 +171,12 @@ export abstract class StreamRef extends Construct implements logs.ISubscriptionD | |
); | ||
} | ||
|
||
public subscriptionDestination(sourceLogGroup: logs.LogGroup): logs.SubscriptionDestination { | ||
public logSubscriptionDestination(sourceLogGroup: logs.LogGroup): logs.LogSubscriptionDestination { | ||
// Following example from https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#DestinationKinesisExample | ||
if (!this.cloudWatchLogsRole) { | ||
// Create a role to be assumed by CWL that can write to this stream and pass itself. | ||
this.cloudWatchLogsRole = new Role(this, 'CloudWatchLogsCanPutRecords', { | ||
assumedBy: new ServicePrincipal(new FnSub('logs.${AWS::Region}.amazonaws.com')), | ||
assumedBy: new ServicePrincipal(new FnConcat('logs.', new AwsRegion(), '.amazonaws.com')), | ||
}); | ||
this.cloudWatchLogsRole.addToPolicy(new PolicyStatement().addAction('kinesis:PutRecord').addResource(this.streamArn)); | ||
this.cloudWatchLogsRole.addToPolicy(new PolicyStatement().addAction('iam:PassRole').addResource(this.cloudWatchLogsRole.roleArn)); | ||
|
@@ -194,19 +195,35 @@ export abstract class StreamRef extends Construct implements logs.ISubscriptionD | |
return { arn: this.streamArn, role: this.cloudWatchLogsRole }; | ||
} | ||
|
||
if (!sourceStack.env.account || !thisStack.env.account) { | ||
This comment has been minimized.
Sorry, something went wrong.
eladb
Contributor
|
||
throw new Error('SubscriptionFilter stack and Destination stack must either both have accounts defined, or both not have accounts'); | ||
} | ||
|
||
return this.crossAccountLogSubscriptionDestination(sourceLogGroup); | ||
} | ||
|
||
/** | ||
* Generate a CloudWatch Logs Destination and return the properties in the form o a subscription destination | ||
*/ | ||
private crossAccountLogSubscriptionDestination(sourceLogGroup: logs.LogGroup): logs.LogSubscriptionDestination { | ||
const sourceStack = Stack.find(sourceLogGroup); | ||
|
||
// Take some effort to construct a unique ID for the destination that is unique to the | ||
// combination of (stream, loggroup). | ||
const uniqueId = new HashedAddressingScheme().allocateAddress([sourceLogGroup.path.replace('/', ''), sourceStack.env.account!]); | ||
|
||
// The destination lives in the target account | ||
const dest = new logs.CrossAccountDestination(this, 'CloudWatchCrossAccountDestination', { | ||
// Unfortunately destinationName is required so we have to invent one that won't conflict. | ||
destinationName: new FnConcat(sourceLogGroup.logGroupName, 'To', this.streamName) as any, | ||
const dest = new logs.CrossAccountDestination(this, `CWLDestination${uniqueId}`, { | ||
targetArn: this.streamArn, | ||
role: this.cloudWatchLogsRole | ||
role: this.cloudWatchLogsRole! | ||
}); | ||
|
||
dest.addToPolicy(new PolicyStatement() | ||
.addAction('logs:PutSubscriptionFilter') | ||
.addAwsAccountPrincipal(sourceStack.env.account) | ||
.addAllResources()); | ||
|
||
return dest.subscriptionDestination(sourceLogGroup); | ||
return dest.logSubscriptionDestination(sourceLogGroup); | ||
} | ||
|
||
private grant(identity: IIdentityResource, actions: { streamActions: string[], keyActions: string[] }) { | ||
|
@@ -364,9 +381,8 @@ class ImportedStreamRef extends StreamRef { | |
super(parent, name); | ||
|
||
this.streamArn = props.streamArn; | ||
// ARN always looks like: arn:aws:kinesis:us-east-2:123456789012:stream/mystream | ||
// so we can get the name from the ARN. | ||
this.streamName = new FnSelect(1, new FnSplit('/', this.streamArn)); | ||
// Get the name from the ARN | ||
this.streamName = Arn.parseToken(props.streamArn).resourceName; | ||
|
||
if (props.encryptionKey) { | ||
this.encryptionKey = kms.EncryptionKeyRef.import(parent, 'Key', props.encryptionKey); | ||
|
Provide an example