Skip to content

Commit ca5ee35

Browse files
author
Elad Ben-Israel
authored
fix(core): automatic cross-stack refs for CFN resources (#1510)
The "toCloudFormation" method of generated CFN resources invoke "resolve" so that any lazy tokens are evaluated. This escaped the global settings set by `findTokens` which collect tokens so they can be reported as references by `StackElement.prepare`. To solve this, findTokens now accepts a function instead of an object and basically just wraps it's invocation with settings that will collect all tokens resolved within that scope. Not an ideal solution but simple enough. This was not discovered because we didn't have any tests that validated the behavior of the generated CFN resources (they are not accessible from the @aws-cdk/cdk library). We add a unit test in the IAM library to cover this use case.
1 parent b909dcd commit ca5ee35

File tree

3 files changed

+59
-7
lines changed

3 files changed

+59
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect } from '@aws-cdk/assert';
2+
import cdk = require('@aws-cdk/cdk');
3+
import { Test } from 'nodeunit';
4+
import iam = require('../lib');
5+
6+
export = {
7+
'automatic exports are created when attributes are referneced across stacks'(test: Test) {
8+
// GIVEN
9+
const stackWithUser = new cdk.Stack();
10+
const stackWithGroup = new cdk.Stack();
11+
const user = new iam.User(stackWithUser, 'User');
12+
const group = new iam.Group(stackWithGroup, 'Group');
13+
14+
// WHEN
15+
group.addUser(user);
16+
17+
//
18+
// `group.addUser` adds the group to the user resource definition, so we expect
19+
// that an automatic export will be created for the group and the user's stack
20+
// to use ImportValue to import it.
21+
// note that order of "expect"s matters. we first need to synthesize the user's
22+
// stack so that the cross stack reference will be reported and only then the
23+
// group's stack. in the real world, App will take care of this.
24+
//
25+
26+
// THEN
27+
expect(stackWithUser).toMatch({
28+
Resources: {
29+
User00B015A1: {
30+
Type: "AWS::IAM::User",
31+
Properties: {
32+
Groups: [ { "Fn::ImportValue": "ExportsOutputRefGroupC77FDACD8CF7DD5B" } ]
33+
}
34+
}
35+
}
36+
});
37+
expect(stackWithGroup).toMatch({
38+
Outputs: {
39+
ExportsOutputRefGroupC77FDACD8CF7DD5B: {
40+
Value: { Ref: "GroupC77FDACD" },
41+
Export: { Name: "ExportsOutputRefGroupC77FDACD8CF7DD5B" }
42+
}
43+
},
44+
Resources: {
45+
GroupC77FDACD: {
46+
Type: "AWS::IAM::Group"
47+
}
48+
}
49+
});
50+
test.done();
51+
}
52+
};

packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,7 @@ export abstract class StackElement extends Construct implements IDependable {
128128
// This does make the assumption that the error will not be rectified,
129129
// but the error will be thrown later on anyway. If the error doesn't
130130
// get thrown down the line, we may miss references.
131-
this.node.recordReference(...findTokens(this.toCloudFormation(), {
132-
scope: this,
133-
prefix: []
134-
}));
131+
this.node.recordReference(...findTokens(this, () => this.toCloudFormation()));
135132
} catch (e) {
136133
if (e.type !== 'CfnSynthesisError') { throw e; }
137134
}

packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { IConstruct } from '../construct';
12
import { containsListToken, TOKEN_MAP } from "./encoding";
23
import { RESOLVE_OPTIONS } from "./options";
34
import { RESOLVE_METHOD, ResolveContext, Token } from "./token";
@@ -120,13 +121,15 @@ export function resolve(obj: any, context: ResolveContext): any {
120121
/**
121122
* Find all Tokens that are used in the given structure
122123
*/
123-
export function findTokens(obj: any, context: ResolveContext): Token[] {
124+
export function findTokens(scope: IConstruct, fn: () => any): Token[] {
124125
const ret = new Array<Token>();
125126

126127
const options = RESOLVE_OPTIONS.push({ collect: ret.push.bind(ret) });
127128
try {
128-
// resolve() for side effect of calling 'preProcess', which adds to the
129-
resolve(obj, context);
129+
resolve(fn(), {
130+
scope,
131+
prefix: []
132+
});
130133
} finally {
131134
options.pop();
132135
}

0 commit comments

Comments
 (0)