Skip to content

Commit 3cca03d

Browse files
authored
feat: publish construct tree into the cloud assembly (#4194)
Publish the CDK construct tree with the construct id and path to the cloud assembly directory, with a corresponding manifest entry. Motivation and proposal documented in RFC - #4053. This change focuses on integrating generating the metadata into the cloud assembly as part of the the 'synthesize' lifecycle phase. Subsequent changes will enrich the output with additional information about each construct (see RFC).
1 parent 87f096e commit 3cca03d

File tree

21 files changed

+357
-57
lines changed

21 files changed

+357
-57
lines changed

packages/@aws-cdk/assets/test/test.staging.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export = {
5353
'asset.af10ac04b3b607b0f8659c8f0cee8c343025ee75baf0b146f10f0e5311d2c46b.gz',
5454
'cdk.out',
5555
'manifest.json',
56-
'stack.template.json'
56+
'stack.template.json',
57+
'tree.json',
5758
]);
5859
test.done();
5960
}

packages/@aws-cdk/aws-route53/test/test.route53.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,15 @@ export = {
219219

220220
class TestApp {
221221
public readonly stack: cdk.Stack;
222-
private readonly app = new cdk.App();
222+
private readonly app: cdk.App;
223223

224224
constructor() {
225225
const account = '123456789012';
226226
const region = 'bermuda-triangle';
227-
this.app.node.setContext(`availability-zones:${account}:${region}`,
228-
[`${region}-1a`]);
227+
const context = {
228+
[`availability-zones:${account}:${region}`]: `${region}-1a`
229+
};
230+
this.app = new cdk.App({ context });
229231
this.stack = new cdk.Stack(this.app, 'MyStack', { env: { account, region } });
230232
}
231233
}

packages/@aws-cdk/core/lib/app.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import cxapi = require('@aws-cdk/cx-api');
22
import { CloudAssembly } from '@aws-cdk/cx-api';
33
import { Construct, ConstructNode } from './construct';
44
import { collectRuntimeInformation } from './private/runtime-info';
5+
import { TreeMetadata } from './private/tree-metadata';
56

67
const APP_SYMBOL = Symbol.for('@aws-cdk/core.App');
78

@@ -49,6 +50,13 @@ export interface AppProps {
4950
* @default - no additional context
5051
*/
5152
readonly context?: { [key: string]: string };
53+
54+
/**
55+
* Include construct tree metadata as part of the Cloud Assembly.
56+
*
57+
* @default true
58+
*/
59+
readonly treeMetadata?: boolean;
5260
}
5361

5462
/**
@@ -110,6 +118,10 @@ export class App extends Construct {
110118
// doesn't bite manual calling of the function.
111119
process.once('beforeExit', () => this.synth());
112120
}
121+
122+
if (props.treeMetadata === undefined || props.treeMetadata) {
123+
new TreeMetadata(this);
124+
}
113125
}
114126

115127
/**
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import fs = require('fs');
2+
import path = require('path');
3+
4+
import { ArtifactType } from '@aws-cdk/cx-api';
5+
import { Construct, IConstruct, ISynthesisSession } from '../construct';
6+
7+
const FILE_PATH = 'tree.json';
8+
9+
/**
10+
* Construct that is automatically attached to the top-level `App`.
11+
* This generates, as part of synthesis, a file containing the construct tree and the metadata for each node in the tree.
12+
* The output is in a tree format so as to preserve the construct hierarchy.
13+
*
14+
* @experimental
15+
*/
16+
export class TreeMetadata extends Construct {
17+
constructor(scope: Construct) {
18+
super(scope, 'Tree');
19+
}
20+
21+
protected synthesize(session: ISynthesisSession) {
22+
const lookup: { [path: string]: Node } = { };
23+
24+
const visit = (construct: IConstruct): Node => {
25+
const children = construct.node.children.map(visit);
26+
const node: Node = {
27+
id: construct.node.id || 'App',
28+
path: construct.node.path,
29+
children: children.length === 0 ? undefined : children,
30+
};
31+
32+
lookup[node.path] = node;
33+
34+
return node;
35+
};
36+
37+
const tree = {
38+
version: 'tree-0.1',
39+
tree: visit(this.node.root),
40+
};
41+
42+
const builder = session.assembly;
43+
fs.writeFileSync(path.join(builder.outdir, FILE_PATH), JSON.stringify(tree, undefined, 2), { encoding: 'utf-8' });
44+
45+
builder.addArtifact('Tree', {
46+
type: ArtifactType.CDK_TREE,
47+
properties: {
48+
file: FILE_PATH
49+
}
50+
});
51+
}
52+
}
53+
54+
interface Node {
55+
id: string;
56+
path: string;
57+
children?: Node[];
58+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import fs = require('fs');
2+
import { Test } from 'nodeunit';
3+
import path = require('path');
4+
import { App, Construct, Resource, Stack } from '../../lib/index';
5+
6+
export = {
7+
'tree metadata is generated as expected'(test: Test) {
8+
const app = new App();
9+
10+
const stack = new Stack(app, 'mystack');
11+
new Construct(stack, 'group1');
12+
const group2 = new Construct(stack, 'group2');
13+
14+
new MyResource(group2, 'resource3');
15+
16+
const assembly = app.synth();
17+
const treeArtifact = assembly.tree();
18+
test.ok(treeArtifact);
19+
20+
test.deepEqual(readJson(assembly.directory, treeArtifact!.file), {
21+
version: 'tree-0.1',
22+
tree: {
23+
id: 'App',
24+
path: '',
25+
children: [
26+
{
27+
id: 'Tree',
28+
path: 'Tree'
29+
},
30+
{
31+
id: 'mystack',
32+
path: 'mystack',
33+
children: [
34+
{
35+
id: 'group1',
36+
path: 'mystack/group1'
37+
},
38+
{
39+
id: 'group2',
40+
path: 'mystack/group2',
41+
children: [
42+
{ id: 'resource3', path: 'mystack/group2/resource3' }
43+
]
44+
}
45+
]
46+
},
47+
]
48+
}
49+
});
50+
test.done();
51+
},
52+
};
53+
54+
class MyResource extends Resource {
55+
constructor(scope: Construct, id: string) {
56+
super(scope, id);
57+
}
58+
}
59+
60+
function readJson(outdir: string, file: string) {
61+
return JSON.parse(fs.readFileSync(path.join(outdir, file), 'utf-8'));
62+
}

packages/@aws-cdk/core/test/test.app.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@ export = {
125125
},
126126

127127
'setContext(k,v) can be used to set context programmatically'(test: Test) {
128-
const prog = new App();
129-
prog.node.setContext('foo', 'bar');
128+
const prog = new App({
129+
context: {
130+
foo: 'bar'
131+
}
132+
});
130133
test.deepEqual(prog.node.tryGetContext('foo'), 'bar');
131134
test.done();
132135
},

packages/@aws-cdk/core/test/test.construct.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ import { Test } from 'nodeunit';
33
import { App as Root, Aws, Construct, ConstructNode, ConstructOrder, IConstruct, Lazy, ValidationError } from '../lib';
44

55
// tslint:disable:variable-name
6-
// tslint:disable:max-line-length
76

87
export = {
98
'the "Root" construct is a special construct which can be used as the root of the tree'(test: Test) {
109
const root = new Root();
1110
test.equal(root.node.id, '', 'if not specified, name of a root construct is an empty string');
1211
test.ok(!root.node.scope, 'no parent');
13-
test.equal(root.node.children.length, 0, 'a construct is created without children'); // no children
12+
test.equal(root.node.children.length, 1);
1413
test.done();
1514
},
1615

@@ -100,7 +99,7 @@ export = {
10099
const child = new Construct(root, 'Child1');
101100
new Construct(root, 'Child2');
102101
test.equal(child.node.children.length, 0, 'no children');
103-
test.equal(root.node.children.length, 2, 'two children are expected');
102+
test.equal(root.node.children.length, 3, 'three children are expected');
104103
test.done();
105104
},
106105

@@ -126,9 +125,9 @@ export = {
126125
const t = createTree();
127126

128127
test.equal(t.root.toString(), '<root>');
129-
test.equal(t.child1_1_1.toString(), 'Child1/Child11/Child111');
130-
test.equal(t.child2.toString(), 'Child2');
131-
test.equal(toTreeString(t.root), 'App\n Construct [Child1]\n Construct [Child11]\n Construct [Child111]\n Construct [Child12]\n Construct [Child2]\n Construct [Child21]\n');
128+
test.equal(t.child1_1_1.toString(), 'HighChild/Child1/Child11/Child111');
129+
test.equal(t.child2.toString(), 'HighChild/Child2');
130+
test.equal(toTreeString(t.root), 'App\n TreeMetadata [Tree]\n Construct [HighChild]\n Construct [Child1]\n Construct [Child11]\n Construct [Child111]\n Construct [Child12]\n Construct [Child2]\n Construct [Child21]\n');
132131
test.done();
133132
},
134133

@@ -139,28 +138,30 @@ export = {
139138
};
140139

141140
const t = createTree(context);
142-
test.equal(t.root.node.tryGetContext('ctx1'), 12);
141+
test.equal(t.child1_2.node.tryGetContext('ctx1'), 12);
143142
test.equal(t.child1_1_1.node.tryGetContext('ctx2'), 'hello');
144143
test.done();
145144
},
146145

146+
// tslint:disable-next-line:max-line-length
147147
'construct.setContext(k,v) sets context at some level and construct.getContext(key) will return the lowermost value defined in the stack'(test: Test) {
148148
const root = new Root();
149-
root.node.setContext('c1', 'root');
150-
root.node.setContext('c2', 'root');
149+
const highChild = new Construct(root, 'highChild');
150+
highChild.node.setContext('c1', 'root');
151+
highChild.node.setContext('c2', 'root');
151152

152-
const child1 = new Construct(root, 'child1');
153+
const child1 = new Construct(highChild, 'child1');
153154
child1.node.setContext('c2', 'child1');
154155
child1.node.setContext('c3', 'child1');
155156

156-
const child2 = new Construct(root, 'child2');
157+
const child2 = new Construct(highChild, 'child2');
157158
const child3 = new Construct(child1, 'child1child1');
158159
child3.node.setContext('c1', 'child3');
159160
child3.node.setContext('c4', 'child3');
160161

161-
test.equal(root.node.tryGetContext('c1'), 'root');
162-
test.equal(root.node.tryGetContext('c2'), 'root');
163-
test.equal(root.node.tryGetContext('c3'), undefined);
162+
test.equal(highChild.node.tryGetContext('c1'), 'root');
163+
test.equal(highChild.node.tryGetContext('c2'), 'root');
164+
test.equal(highChild.node.tryGetContext('c3'), undefined);
164165

165166
test.equal(child1.node.tryGetContext('c1'), 'root');
166167
test.equal(child1.node.tryGetContext('c2'), 'child1');
@@ -195,15 +196,15 @@ export = {
195196
'construct.pathParts returns an array of strings of all names from root to node'(test: Test) {
196197
const tree = createTree();
197198
test.deepEqual(tree.root.node.path, '');
198-
test.deepEqual(tree.child1_1_1.node.path, 'Child1/Child11/Child111');
199-
test.deepEqual(tree.child2.node.path, 'Child2');
199+
test.deepEqual(tree.child1_1_1.node.path, 'HighChild/Child1/Child11/Child111');
200+
test.deepEqual(tree.child2.node.path, 'HighChild/Child2');
200201
test.done();
201202
},
202203

203204
'if a root construct has a name, it should be included in the path'(test: Test) {
204205
const tree = createTree({});
205206
test.deepEqual(tree.root.node.path, '');
206-
test.deepEqual(tree.child1_1_1.node.path, 'Child1/Child11/Child111');
207+
test.deepEqual(tree.child1_1_1.node.path, 'HighChild/Child1/Child11/Child111');
207208
test.done();
208209
},
209210

@@ -302,7 +303,7 @@ export = {
302303
new MyBeautifulConstruct(root, 'mbc2');
303304
new MyBeautifulConstruct(root, 'mbc3');
304305
new MyBeautifulConstruct(root, 'mbc4');
305-
test.equal(root.node.children.length, 4);
306+
test.ok(root.node.children.length >= 4);
306307
test.done();
307308
},
308309

@@ -416,7 +417,7 @@ export = {
416417

417418
'ancestors returns a list of parents up to root'(test: Test) {
418419
const { child1_1_1 } = createTree();
419-
test.deepEqual(child1_1_1.node.scopes.map(x => x.node.id), [ '', 'Child1', 'Child11', 'Child111' ]);
420+
test.deepEqual(child1_1_1.node.scopes.map(x => x.node.id), [ '', 'HighChild', 'Child1', 'Child11', 'Child111' ]);
420421
test.done();
421422
},
422423

@@ -481,12 +482,13 @@ export = {
481482

482483
function createTree(context?: any) {
483484
const root = new Root();
485+
const highChild = new Construct(root, 'HighChild');
484486
if (context) {
485-
Object.keys(context).forEach(key => root.node.setContext(key, context[key]));
487+
Object.keys(context).forEach(key => highChild.node.setContext(key, context[key]));
486488
}
487489

488-
const child1 = new Construct(root, 'Child1');
489-
const child2 = new Construct(root, 'Child2');
490+
const child1 = new Construct(highChild, 'Child1');
491+
const child2 = new Construct(highChild, 'Child2');
490492
const child1_1 = new Construct(child1, 'Child11');
491493
const child1_2 = new Construct(child1, 'Child12');
492494
const child1_1_1 = new Construct(child1_1, 'Child111');

0 commit comments

Comments
 (0)