Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: publish construct hierarchy with metadata to cloud assembly #4194

Merged
merged 13 commits into from
Sep 25, 2019
3 changes: 2 additions & 1 deletion packages/@aws-cdk/assets/test/test.staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export = {
'asset.af10ac04b3b607b0f8659c8f0cee8c343025ee75baf0b146f10f0e5311d2c46b.gz',
'cdk.out',
'manifest.json',
'stack.template.json'
'stack.template.json',
'tree.json',
]);
test.done();
}
Expand Down
8 changes: 5 additions & 3 deletions packages/@aws-cdk/aws-route53/test/test.route53.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,15 @@ export = {

class TestApp {
public readonly stack: cdk.Stack;
private readonly app = new cdk.App();
private readonly app: cdk.App;

constructor() {
const account = '123456789012';
const region = 'bermuda-triangle';
this.app.node.setContext(`availability-zones:${account}:${region}`,
[`${region}-1a`]);
const context = {
[`availability-zones:${account}:${region}`]: `${region}-1a`
};
this.app = new cdk.App({ context });
this.stack = new cdk.Stack(this.app, 'MyStack', { env: { account, region } });
}
}
12 changes: 12 additions & 0 deletions packages/@aws-cdk/core/lib/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import cxapi = require('@aws-cdk/cx-api');
import { CloudAssembly } from '@aws-cdk/cx-api';
import { Construct, ConstructNode } from './construct';
import { collectRuntimeInformation } from './private/runtime-info';
import { Tree } from './private/tree';

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

Expand Down Expand Up @@ -49,6 +50,13 @@ export interface AppProps {
* @default - no additional context
*/
readonly context?: { [key: string]: string };

/**
* Include construct tree metadata as part of the Cloud Assembly.
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a "@see" with a link to the RFC

Copy link
Contributor Author

@nija-at nija-at Sep 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I'll wait until the RFC is published so I can get the a more stable link.

* @default true
*/
readonly treeMetadata?: boolean;
}

/**
Expand Down Expand Up @@ -110,6 +118,10 @@ export class App extends Construct {
// doesn't bite manual calling of the function.
process.once('beforeExit', () => this.synth());
}

if (props.treeMetadata === undefined || props.treeMetadata) {
new Tree(this);
}
}

/**
Expand Down
58 changes: 58 additions & 0 deletions packages/@aws-cdk/core/lib/private/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import fs = require('fs');
import path = require('path');

import { ArtifactType } from '@aws-cdk/cx-api';
import { Construct, IConstruct, ISynthesisSession } from '../construct';

const FILE_PATH = 'tree.json';

/**
* Construct that is automatically attached to the top-level `App`.
* This generates, as part of synthesis, a file containing metadata of the `Construct`s in the construct tree.
* The output is in a tree format so as to preserve the construct hierarchy.
*
* @experimental
*/
export class Tree extends Construct {
constructor(scope: Construct) {
super(scope, 'Tree');
}

protected synthesize(session: ISynthesisSession) {
const lookup: { [path: string]: Node } = { };

const visit = (construct: IConstruct): Node => {
const children = construct.node.children.map(visit);
const node: Node = {
id: construct.node.id || 'App',
path: construct.node.path,
children: children.length === 0 ? undefined : children,
};

lookup[node.path] = node;

return node;
};

const tree = {
version: 'tree-0.1',
tree: visit(this.node.root),
};

const builder = session.assembly;
fs.writeFileSync(path.join(builder.outdir, FILE_PATH), JSON.stringify(tree, undefined, 2), { encoding: 'utf-8' });

builder.addArtifact('Tree', {
type: ArtifactType.CDK_METADATA,
properties: {
file: FILE_PATH
}
});
}
}

interface Node {
id: string;
path: string;
children?: Node[];
}
61 changes: 61 additions & 0 deletions packages/@aws-cdk/core/test/private/test.tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import fs = require('fs');
import { Test } from 'nodeunit';
import path = require('path');
import { App, Construct, Resource, Stack } from '../../lib/index';

export = {
'annotations are generated as expected'(test: Test) {
const app = new App();

const stack = new Stack(app, 'mystack');
new Construct(stack, 'group1');
const group2 = new Construct(stack, 'group2');

new MyResource(group2, 'resource3');

const assembly = app.synth();
const annotationsFile = assembly.getMetadata('Tree').file;

test.deepEqual(readJson(assembly.directory, annotationsFile), {
version: 'tree-0.1',
tree: {
id: 'App',
path: '',
children: [
{
id: 'Tree',
path: 'Tree'
},
{
id: 'mystack',
path: 'mystack',
children: [
{
id: 'group1',
path: 'mystack/group1'
},
{
id: 'group2',
path: 'mystack/group2',
children: [
{ id: 'resource3', path: 'mystack/group2/resource3' }
]
}
]
},
]
}
});
test.done();
},
};

class MyResource extends Resource {
constructor(scope: Construct, id: string) {
super(scope, id);
}
}

function readJson(outdir: string, file: string) {
return JSON.parse(fs.readFileSync(path.join(outdir, file), 'utf-8'));
}
7 changes: 5 additions & 2 deletions packages/@aws-cdk/core/test/test.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ export = {
},

'setContext(k,v) can be used to set context programmatically'(test: Test) {
const prog = new App();
prog.node.setContext('foo', 'bar');
const prog = new App({
context: {
foo: 'bar'
}
});
test.deepEqual(prog.node.tryGetContext('foo'), 'bar');
test.done();
},
Expand Down
46 changes: 24 additions & 22 deletions packages/@aws-cdk/core/test/test.construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ import { Test } from 'nodeunit';
import { App as Root, Aws, Construct, ConstructNode, ConstructOrder, IConstruct, Lazy, ValidationError } from '../lib';

// tslint:disable:variable-name
// tslint:disable:max-line-length

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

Expand Down Expand Up @@ -100,7 +99,7 @@ export = {
const child = new Construct(root, 'Child1');
new Construct(root, 'Child2');
test.equal(child.node.children.length, 0, 'no children');
test.equal(root.node.children.length, 2, 'two children are expected');
test.equal(root.node.children.length, 3, 'three children are expected');
test.done();
},

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

test.equal(t.root.toString(), '<root>');
test.equal(t.child1_1_1.toString(), 'Child1/Child11/Child111');
test.equal(t.child2.toString(), 'Child2');
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');
test.equal(t.child1_1_1.toString(), 'HighChild/Child1/Child11/Child111');
test.equal(t.child2.toString(), 'HighChild/Child2');
test.equal(toTreeString(t.root), 'App\n Tree [Tree]\n Construct [HighChild]\n Construct [Child1]\n Construct [Child11]\n Construct [Child111]\n Construct [Child12]\n Construct [Child2]\n Construct [Child21]\n');
test.done();
},

Expand All @@ -139,28 +138,30 @@ export = {
};

const t = createTree(context);
test.equal(t.root.node.tryGetContext('ctx1'), 12);
test.equal(t.child1_2.node.tryGetContext('ctx1'), 12);
test.equal(t.child1_1_1.node.tryGetContext('ctx2'), 'hello');
test.done();
},

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

const child1 = new Construct(root, 'child1');
const child1 = new Construct(highChild, 'child1');
child1.node.setContext('c2', 'child1');
child1.node.setContext('c3', 'child1');

const child2 = new Construct(root, 'child2');
const child2 = new Construct(highChild, 'child2');
const child3 = new Construct(child1, 'child1child1');
child3.node.setContext('c1', 'child3');
child3.node.setContext('c4', 'child3');

test.equal(root.node.tryGetContext('c1'), 'root');
test.equal(root.node.tryGetContext('c2'), 'root');
test.equal(root.node.tryGetContext('c3'), undefined);
test.equal(highChild.node.tryGetContext('c1'), 'root');
test.equal(highChild.node.tryGetContext('c2'), 'root');
test.equal(highChild.node.tryGetContext('c3'), undefined);

test.equal(child1.node.tryGetContext('c1'), 'root');
test.equal(child1.node.tryGetContext('c2'), 'child1');
Expand Down Expand Up @@ -195,15 +196,15 @@ export = {
'construct.pathParts returns an array of strings of all names from root to node'(test: Test) {
const tree = createTree();
test.deepEqual(tree.root.node.path, '');
test.deepEqual(tree.child1_1_1.node.path, 'Child1/Child11/Child111');
test.deepEqual(tree.child2.node.path, 'Child2');
test.deepEqual(tree.child1_1_1.node.path, 'HighChild/Child1/Child11/Child111');
test.deepEqual(tree.child2.node.path, 'HighChild/Child2');
test.done();
},

'if a root construct has a name, it should be included in the path'(test: Test) {
const tree = createTree({});
test.deepEqual(tree.root.node.path, '');
test.deepEqual(tree.child1_1_1.node.path, 'Child1/Child11/Child111');
test.deepEqual(tree.child1_1_1.node.path, 'HighChild/Child1/Child11/Child111');
test.done();
},

Expand Down Expand Up @@ -302,7 +303,7 @@ export = {
new MyBeautifulConstruct(root, 'mbc2');
new MyBeautifulConstruct(root, 'mbc3');
new MyBeautifulConstruct(root, 'mbc4');
test.equal(root.node.children.length, 4);
test.ok(root.node.children.length >= 4);
test.done();
},

Expand Down Expand Up @@ -416,7 +417,7 @@ export = {

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

Expand Down Expand Up @@ -481,12 +482,13 @@ export = {

function createTree(context?: any) {
const root = new Root();
const highChild = new Construct(root, 'HighChild');
if (context) {
Object.keys(context).forEach(key => root.node.setContext(key, context[key]));
Object.keys(context).forEach(key => highChild.node.setContext(key, context[key]));
}

const child1 = new Construct(root, 'Child1');
const child2 = new Construct(root, 'Child2');
const child1 = new Construct(highChild, 'Child1');
const child2 = new Construct(highChild, 'Child2');
const child1_1 = new Construct(child1, 'Child11');
const child1_2 = new Construct(child1, 'Child12');
const child1_1_1 = new Construct(child1_1, 'Child111');
Expand Down
Loading