Skip to content

Commit

Permalink
feat(core): add resource type and properties for all CfnResource cons…
Browse files Browse the repository at this point in the history
…tructs to tree.json (#4894)

Modifies the children node from an array to an object with each child object keyed on its id. Also added an interface `IInspectable` that constructs can optionally implement to contribute attributes into `tree.json`.

Generated classes for Cfn resources implement `IInspectable` and contribute their resource type and props in the attribute bag.

Supercedes #4562
  • Loading branch information
shivlaks authored Nov 11, 2019
1 parent dca9a24 commit 4037155
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 33 deletions.
2 changes: 2 additions & 0 deletions packages/@aws-cdk/core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export * from './resource';
export * from './physical-name';
export * from './assets';

export * from './tree';

// WARNING: Should not be exported, but currently is because of a bug. See the
// class description for more information.
export * from './private/intrinsic';
24 changes: 22 additions & 2 deletions packages/@aws-cdk/core/lib/private/tree-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path = require('path');

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

const FILE_PATH = 'tree.json';

Expand All @@ -23,10 +24,12 @@ export class TreeMetadata extends Construct {

const visit = (construct: IConstruct): Node => {
const children = construct.node.children.map(visit);
const childrenMap = children.reduce((map, child) => Object.assign(map, { [child.id]: child }), {});
const node: Node = {
id: construct.node.id || 'App',
path: construct.node.path,
children: children.length === 0 ? undefined : children,
children: children.length === 0 ? undefined : childrenMap,
attributes: this.getAttributes(construct)
};

lookup[node.path] = node;
Expand All @@ -49,10 +52,27 @@ export class TreeMetadata extends Construct {
}
});
}

private getAttributes(construct: IConstruct): { [key: string]: any } | undefined {
// check if a construct implements IInspectable
function canInspect(inspectable: any): inspectable is IInspectable {
return inspectable.inspect !== undefined;
}

const inspector = new TreeInspector();

// get attributes from the inspector
if (canInspect(construct)) {
construct.inspect(inspector);
return inspector.attributes;
}
return undefined;
}
}

interface Node {
id: string;
path: string;
children?: Node[];
children?: { [key: string]: Node };
attributes?: { [key: string]: any };
}
33 changes: 33 additions & 0 deletions packages/@aws-cdk/core/lib/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Inspector that maintains an attribute bag
*/
export class TreeInspector {
/**
* Represents the bag of attributes as key-value pairs.
*/
public readonly attributes: { [key: string]: any } = {};

/**
* Adds attribute to bag. Keys should be added by convention to prevent conflicts
* i.e. L1 constructs will contain attributes with keys prefixed with aws:cdk:cloudformation
*
* @param key - key for metadata
* @param value - value of metadata.
*/
public addAttribute(key: string, value: any) {
this.attributes[key] = value;
}
}

/**
* Interface for examining a construct and exposing metadata.
*
*/
export interface IInspectable {
/**
* Examines construct
*
* @param inspector - tree inspector to collect and process attributes
*/
inspect(inspector: TreeInspector): void;
}
108 changes: 81 additions & 27 deletions packages/@aws-cdk/core/test/private/test.tree-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import fs = require('fs');
import { Test } from 'nodeunit';
import path = require('path');
import { App, Construct, Resource, Stack } from '../../lib/index';
import { App, CfnResource, Construct, Stack, TreeInspector } from '../../lib/index';

export = {
'tree metadata is 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');
new Construct(stack, 'myconstruct');

const assembly = app.synth();
const treeArtifact = assembly.tree();
Expand All @@ -22,40 +19,97 @@ export = {
tree: {
id: 'App',
path: '',
children: [
{
children: {
Tree: {
id: 'Tree',
path: 'Tree'
},
{
mystack: {
id: 'mystack',
path: 'mystack',
children: [
{
id: 'group1',
path: 'mystack/group1'
},
{
id: 'group2',
path: 'mystack/group2',
children: [
{ id: 'resource3', path: 'mystack/group2/resource3' }
]
children: {
myconstruct: {
id: 'myconstruct',
path: 'mystack/myconstruct'
}
]
},
]
}
}
}
}
});
test.done();
},
};

class MyResource extends Resource {
constructor(scope: Construct, id: string) {
super(scope, id);
'tree metadata for a Cfn resource'(test: Test) {
class MyCfnResource extends CfnResource {
constructor(scope: Construct, id: string) {
super(scope, id, {
type: 'CDK::UnitTest::MyCfnResource'
});
}

public inspect(inspector: TreeInspector) {
inspector.addAttribute('aws:cdk:cloudformation:type', 'CDK::UnitTest::MyCfnResource');
inspector.addAttribute('aws:cdk:cloudformation:props', this.cfnProperties);
}

protected get cfnProperties(): { [key: string]: any } {
return {
mystringpropkey: 'mystringpropval',
mylistpropkey: ['listitem1'],
mystructpropkey: {
myboolpropkey: true,
mynumpropkey: 50
}
};
}
}

const app = new App();
const stack = new Stack(app, 'mystack');
new MyCfnResource(stack, 'mycfnresource');

const assembly = app.synth();
const treeArtifact = assembly.tree();
test.ok(treeArtifact);

test.deepEqual(readJson(assembly.directory, treeArtifact!.file), {
version: 'tree-0.1',
tree: {
id: 'App',
path: '',
children: {
Tree: {
id: 'Tree',
path: 'Tree'
},
mystack: {
id: 'mystack',
path: 'mystack',
children: {
mycfnresource: {
id: 'mycfnresource',
path: 'mystack/mycfnresource',
attributes: {
'aws:cdk:cloudformation:type': 'CDK::UnitTest::MyCfnResource',
'aws:cdk:cloudformation:props': {
mystringpropkey: 'mystringpropval',
mylistpropkey: ['listitem1'],
mystructpropkey: {
myboolpropkey: true,
mynumpropkey: 50
}
}
}
}
}
}
}
}
});
test.done();
}
}
};

function readJson(outdir: string, file: string) {
return JSON.parse(fs.readFileSync(path.join(outdir, file), 'utf-8'));
Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/core/test/test.synthesis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export = {
tree: {
id: 'App',
path: '',
children: [
{ id: 'Tree', path: 'Tree' }
]
children: {
Tree: { id: 'Tree', path: 'Tree' }
}
}
});
test.done();
Expand Down
35 changes: 34 additions & 1 deletion tools/cfn2ts/lib/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const CONSTRUCT_CLASS = `${CORE}.Construct`;
const TAG_TYPE = `${CORE}.TagType`;
const TAG_MANAGER = `${CORE}.TagManager`;

enum TreeAttributes {
CFN_TYPE = 'aws:cdk:cloudformation:type',
CFN_PROPS = 'aws:cdk:cloudformation:props'
}

interface Dictionary<T> { [key: string]: T; }

/**
Expand Down Expand Up @@ -104,7 +109,8 @@ export default class CodeGenerator {

private openClass(name: genspec.CodeName, superClasses?: string): string {
const extendsPostfix = superClasses ? ` extends ${superClasses}` : '';
this.code.openBlock(`export class ${name.className}${extendsPostfix}`);
const implementsPostfix = ` implements ${CORE}.IInspectable`;
this.code.openBlock(`export class ${name.className}${extendsPostfix}${implementsPostfix}`);
return name.className;
}

Expand Down Expand Up @@ -305,6 +311,9 @@ export default class CodeGenerator {
}
this.code.closeBlock();

this.code.line();
this.emitTreeAttributes(resourceName);

// setup render properties
if (propsType && propMap) {
this.code.line();
Expand Down Expand Up @@ -339,6 +348,30 @@ export default class CodeGenerator {
this.code.closeBlock();
}

/**
* Emit the function that is going to implement the IInspectable interface.
*
* The generated code looks like this:
* public inspect(inspector: cdk.TreeInspector) {
* inspector.addAttribute("aws:cdk:cloudformation:type", CfnManagedPolicy.CFN_RESOURCE_TYPE_NAME);
* inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties);
* }
*
*/
private emitTreeAttributes(resource: genspec.CodeName): void {
this.code.line('/**');
this.code.line(' * Examines the CloudFormation resource and discloses attributes.');
this.code.line(' *');
this.code.line(' * @param inspector - tree inspector to collect and process attributes');
this.code.line(' *');
this.code.line(' * @stability experimental');
this.code.line(' */');
this.code.openBlock(`public inspect(inspector: ${CORE}.TreeInspector)`);
this.code.line(`inspector.addAttribute("${TreeAttributes.CFN_TYPE}", ${resource.className}.CFN_RESOURCE_TYPE_NAME);`);
this.code.line(`inspector.addAttribute("${TreeAttributes.CFN_PROPS}", this.cfnProperties);`);
this.code.closeBlock();
}

/**
* Emit the function that is going to map the generated TypeScript object back into the schema that CloudFormation expects
*
Expand Down

0 comments on commit 4037155

Please sign in to comment.