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(core): new APIs for Aspects and Tags #9558

Merged
merged 3 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/@aws-cdk/core/lib/aspect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { IConstruct } from './construct-compat';

const ASPECTS_SYMBOL = Symbol('cdk-aspects');

/**
* Represents an Aspect
*/
Expand All @@ -9,3 +11,48 @@ export interface IAspect {
*/
visit(node: IConstruct): void;
}

/**
* Aspects can be applied to CDK tree scopes and can operate on the tree before
* synthesis.
*/
export class Aspects {

/**
* Returns the `Aspects` object associated with a construct scope.
* @param scope The scope for which these aspects will apply.
*/
public static of(scope: IConstruct): Aspects {
let aspects = (scope as any)[ASPECTS_SYMBOL];
if (!aspects) {
aspects = new Aspects(scope);

Object.defineProperty(scope, ASPECTS_SYMBOL, {
value: aspects,
configurable: false,
enumerable: false,
});
}
return aspects;
}

// TODO(2.0): private readonly _aspects = new Array<IAspect>();
private constructor(private readonly scope: IConstruct) { }

/**
* Adds an aspect to apply this scope before synthesis.
* @param aspect The aspect to add.
*/
public add(aspect: IAspect) {
// TODO(2.0): this._aspects.push(aspect);
this.scope.node._actualNode.applyAspect(aspect);
}

/**
* The list of aspects which were directly applied on this scope.
*/
public get aspects(): IAspect[] {
// TODO(2.0): return [ ...this._aspects ];
return [ ...(this.scope.node._actualNode as any)._aspects ]; // clone
}
}
15 changes: 12 additions & 3 deletions packages/@aws-cdk/core/lib/construct-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cxapi from '@aws-cdk/cx-api';
import * as constructs from 'constructs';
import { IAspect } from './aspect';
import { IAspect, Aspects } from './aspect';
import { IDependable } from './dependency';
import { Token } from './token';

Expand Down Expand Up @@ -267,7 +267,13 @@ export class ConstructNode {
*/
public readonly _actualNode: constructs.Node;

/**
* The Construct class that hosts this API.
*/
private readonly host: Construct;

constructor(host: Construct, scope: IConstruct, id: string) {
this.host = host;
this._actualNode = new constructs.Node(host, scope, id);

// store a back reference on _actualNode so we can our ConstructNode from it
Expand Down Expand Up @@ -433,9 +439,12 @@ export class ConstructNode {
}

/**
* Applies the aspect to this Constructs node
* DEPRECATED: Applies the aspect to this Constructs node
*
* @deprecated This API is going to be removed in the next major version of
* the AWS CDK. Please use `Aspects.of(scope).add()` instead.
*/
public applyAspect(aspect: IAspect): void { this._actualNode.applyAspect(aspect); }
public applyAspect(aspect: IAspect): void { Aspects.of(this.host).add(aspect); }

/**
* All parent scopes of this construct.
Expand Down
36 changes: 18 additions & 18 deletions packages/@aws-cdk/core/lib/private/synthesis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as cxapi from '@aws-cdk/cx-api';
import * as constructs from 'constructs';
import { Aspects, IAspect } from '../aspect';
import { Construct, IConstruct, SynthesisOptions, ValidationError } from '../construct-compat';
import { Stack } from '../stack';
import { Stage, StageSynthesisOptions } from '../stage';
Expand Down Expand Up @@ -60,26 +61,35 @@ function synthNestedAssemblies(root: IConstruct, options: StageSynthesisOptions)
* twice for the same construct.
*/
function invokeAspects(root: IConstruct) {
const invokedByPath: { [nodePath: string]: IAspect[] } = { };

let nestedAspectWarning = false;
recurse(root, []);

function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) {
// hackery to be able to access some private members with strong types (yack!)
const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any;

const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects];
const nodeAspectsCount = node._aspects.length;
const node = construct.node;
const aspects = Aspects.of(construct);
const allAspectsHere = [...inheritedAspects ?? [], ...aspects.aspects];
const nodeAspectsCount = aspects.aspects.length;
for (const aspect of allAspectsHere) {
if (node.invokedAspects.includes(aspect)) { continue; }
let invoked = invokedByPath[node.path];
if (!invoked) {
invoked = invokedByPath[node.path] = [];
}

if (invoked.includes(aspect)) { continue; }

aspect.visit(construct);

// if an aspect was added to the node while invoking another aspect it will not be invoked, emit a warning
// the `nestedAspectWarning` flag is used to prevent the warning from being emitted for every child
if (!nestedAspectWarning && nodeAspectsCount !== node._aspects.length) {
if (!nestedAspectWarning && nodeAspectsCount !== aspects.aspects.length) {
construct.node.addWarning('We detected an Aspect was added via another Aspect, and will not be applied');
nestedAspectWarning = true;
}
node.invokedAspects.push(aspect);

// mark as invoked for this node
invoked.push(aspect);
}

for (const child of construct.node.children) {
Expand Down Expand Up @@ -180,13 +190,3 @@ interface IProtectedConstructMethods extends IConstruct {
*/
onPrepare(): void;
}

/**
* The constructs Node type, but with some aspects-related fields public.
*
* Hackery!
*/
type NodeWithAspectPrivatesHangingOut = Omit<constructs.Node, 'invokedAspects' | '_aspects'> & {
readonly invokedAspects: constructs.IAspect[];
readonly _aspects: constructs.IAspect[];
};
43 changes: 38 additions & 5 deletions packages/@aws-cdk/core/lib/tag-aspect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// import * as cxapi from '@aws-cdk/cx-api';
import { IAspect } from './aspect';
import { IAspect, Aspects } from './aspect';
import { Construct, IConstruct } from './construct-compat';
import { ITaggable, TagManager } from './tag-manager';

Expand Down Expand Up @@ -86,17 +86,21 @@ abstract class TagBase implements IAspect {
export class Tag extends TagBase {

/**
* add tags to the node of a construct and all its the taggable children
* DEPRECATED: add tags to the node of a construct and all its the taggable children
*
* @deprecated use `Tags.of(scope).add()`
*/
public static add(scope: Construct, key: string, value: string, props: TagProps = {}) {
scope.node.applyAspect(new Tag(key, value, props));
Tags.of(scope).add(key, value, props);
}

/**
* remove tags to the node of a construct and all its the taggable children
* DEPRECATED: remove tags to the node of a construct and all its the taggable children
*
* @deprecated use `Tags.of(scope).remove()`
*/
public static remove(scope: Construct, key: string, props: TagProps = {}) {
scope.node.applyAspect(new RemoveTag(key, props));
Tags.of(scope).remove(key, props);
}

/**
Expand Down Expand Up @@ -126,6 +130,35 @@ export class Tag extends TagBase {
}
}

/**
* Manages AWS tags for all resources within a construct scope.
*/
export class Tags {
/**
* Returns the tags API for this scope.
* @param scope The scope
*/
public static of(scope: IConstruct): Tags {
return new Tags(scope);
}

private constructor(private readonly scope: IConstruct) { }

/**
* add tags to the node of a construct and all its the taggable children
*/
public add(key: string, value: string, props: TagProps = {}) {
Aspects.of(this.scope).add(new Tag(key, value, props));
}

/**
* remove tags to the node of a construct and all its the taggable children
*/
public remove(key: string, props: TagProps = {}) {
Aspects.of(this.scope).add(new RemoveTag(key, props));
}
}

/**
* The RemoveTag Aspect will handle removing tags from this node and children
*/
Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/core/test/test.aspect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { Test } from 'nodeunit';
import { App } from '../lib';
import { IAspect } from '../lib/aspect';
import { IAspect, Aspects } from '../lib/aspect';
import { Construct, IConstruct } from '../lib/construct-compat';

class MyConstruct extends Construct {
Expand Down Expand Up @@ -29,7 +29,7 @@ export = {
'Aspects are invoked only once'(test: Test) {
const app = new App();
const root = new MyConstruct(app, 'MyConstruct');
root.node.applyAspect(new VisitOnce());
Aspects.of(root).add(new VisitOnce());
app.synth();
test.deepEqual(root.visitCounter, 1);
app.synth();
Expand All @@ -41,9 +41,9 @@ export = {
const app = new App();
const root = new MyConstruct(app, 'MyConstruct');
const child = new MyConstruct(root, 'ChildConstruct');
root.node.applyAspect({
Aspects.of(root).add({
visit(construct: IConstruct) {
construct.node.applyAspect({
Aspects.of(construct).add({
visit(inner: IConstruct) {
inner.node.addMetadata('test', 'would-be-ignored');
},
Expand All @@ -62,7 +62,7 @@ export = {
const app = new App();
const root = new MyConstruct(app, 'Construct');
const child = new MyConstruct(root, 'ChildConstruct');
root.node.applyAspect(new MyAspect());
Aspects.of(root).add(new MyAspect());
app.synth();
test.deepEqual(root.node.metadata[0].type, 'foo');
test.deepEqual(root.node.metadata[0].data, 'bar');
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/core/test/test.stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as cxapi from '@aws-cdk/cx-api';
import { Test } from 'nodeunit';
import {
App, CfnCondition, CfnInclude, CfnOutput, CfnParameter,
CfnResource, Construct, Lazy, ScopedAws, Stack, Tag, validateString, ISynthesisSession } from '../lib';
CfnResource, Construct, Lazy, ScopedAws, Stack, validateString, ISynthesisSession, Tags } from '../lib';
import { Intrinsic } from '../lib/private/intrinsic';
import { resolveReferences } from '../lib/private/refs';
import { PostResolveToken } from '../lib/util';
Expand Down Expand Up @@ -840,7 +840,7 @@ export = {
const stack2 = new Stack(stack1, 'stack2');

// WHEN
Tag.add(app, 'foo', 'bar');
Tags.of(app).add('foo', 'bar');

// THEN
const asm = app.synth();
Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/core/test/test.stage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as cxapi from '@aws-cdk/cx-api';
import { Test } from 'nodeunit';
import { App, CfnResource, Construct, IAspect, IConstruct, Stack, Stage } from '../lib';
import { App, CfnResource, Construct, IAspect, IConstruct, Stack, Stage, Aspects } from '../lib';

export = {
'Stack inherits unspecified part of the env from Stage'(test: Test) {
Expand Down Expand Up @@ -148,7 +148,7 @@ export = {

// WHEN
const aspect = new TouchingAspect();
stack.node.applyAspect(aspect);
Aspects.of(stack).add(aspect);

// THEN
app.synth();
Expand All @@ -168,7 +168,7 @@ export = {

// WHEN
const aspect = new TouchingAspect();
app.node.applyAspect(aspect);
Aspects.of(app).add(aspect);

// THEN
app.synth();
Expand Down
Loading