Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
67 changes: 67 additions & 0 deletions packages/aws-cdk-lib/aws-sns/test/sns.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Template } from '../../assertions';
import { AssertionError } from '../../assertions/lib/private/error';
import * as notifications from '../../aws-codestarnotifications';
import * as iam from '../../aws-iam';
import { ServicePrincipal } from '../../aws-iam';
import * as kms from '../../aws-kms';
import { CfnKey } from '../../aws-kms';
import * as cdk from '../../core';
import { Stage } from '../../core';
import * as sns from '../lib';
import { TopicGrants } from '../lib';

Expand Down Expand Up @@ -1082,4 +1086,67 @@ describe('Topic', () => {
).toThrow('`fifoThroughputScope` can only be set for FIFO SNS topics.');
});
});

/*
This is a representative test suite for source tracing.
What we are asserting here about CfnTopic applies to all L1 constructs.
*/
describe('Source tracing', () => {
test('Metadata contains propertyName and stack trace with CDK_DEBUG=1', () => {
try {
process.env.CDK_DEBUG = '1';
const stack = new cdk.Stack();

const topic = new sns.CfnTopic(stack, 'MyTopic', {
topicName: 'topicName',
});

topic.displayName = 'something';
const lineWherePropertyWasSet = getLineNumber() - 1; // the one before this one

const asm = synth(stack);
const metadata = JSON.parse(fs.readFileSync(path.join(asm.directory, 'Default.metadata.json'), 'utf8'));
const propertyNameEntry = metadata['/Default/MyTopic'].find((e: any) => e.type === 'aws:cdk:propertyName');

expect(propertyNameEntry).toBeDefined();
expect(propertyNameEntry.data).toEqual('DisplayName');
expect(propertyNameEntry.trace.some(
(t: string) => t.includes(`${__filename}:${lineWherePropertyWasSet}`)),
).toBe(true);
} finally {
delete process.env.CDK_DEBUG;
}
});

test('Metadata does not contain propertyName by default', () => {
const stack = new cdk.Stack();

const topic = new sns.CfnTopic(stack, 'MyTopic', {
topicName: 'topicName',
});

topic.displayName = 'something';

const asm = synth(stack);
const metadata = JSON.parse(fs.readFileSync(path.join(asm.directory, 'Default.metadata.json'), 'utf8'));
const propertyNameEntry = metadata['/Default/MyTopic'].find((e: any) => e.type === 'aws:cdk:propertyName');

expect(propertyNameEntry).toBeUndefined();
});
});
});

function synth(stack: cdk.Stack) {
const stage = Stage.of(stack);
if (!Stage.isStage(stage)) {
throw new AssertionError('unexpected: all stacks must be part of a Stage or an App');
}

return stage.synth();
}

function getLineNumber(): number {
const err = new Error();
const line = err.stack?.split('\n')[2]?.match(/:(\d+):\d+\)?$/)?.[1];
return Number(line);
}
2 changes: 2 additions & 0 deletions packages/aws-cdk-lib/core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export * from './eventbridge';

export * from './grants';

export * from './source-tracing';

// WARNING: Should not be exported, but currently is because of a bug. See the
// class description for more information.
export * from './private/intrinsic';
Expand Down
11 changes: 11 additions & 0 deletions packages/aws-cdk-lib/core/lib/source-tracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Node } from 'constructs';
import { debugModeEnabled } from './debug';

export function traceProperty(node: Node, propertyName: string) {
if (debugModeEnabled()) {
node.addMetadata('aws:cdk:propertyName', propertyName, {
stackTrace: true,
traceFromFunction: traceProperty,
});
}
}
1 change: 1 addition & 0 deletions tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class CdkCore extends ExternalModule {
public readonly unionMapper = makeCallableExpr(this, 'unionMapper');
public readonly requireProperty = makeCallableExpr(this, 'requireProperty');
public readonly isResolvableObject = makeCallableExpr(this, 'isResolvableObject');
public readonly traceProperty = makeCallableExpr(this, 'traceProperty');
public readonly mapArrayInPlace = makeCallableExpr(this, 'mapArrayInPlace');

public readonly ValidationResult = $T(Type.fromName(this, 'ValidationResult'));
Expand Down
30 changes: 27 additions & 3 deletions tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,31 @@ export class ResourceClass extends ClassType implements Referenceable {
}

for (const prop of this.decider.classProperties) {
this.addProperty(prop.propertySpec);
const spec = prop.propertySpec;
if (spec.immutable) {
this.addProperty(spec);
} else {
// For mutable properties, generate getter and setter
const backingFieldName = `_${spec.name}`;
this.addProperty({
name: backingFieldName,
type: spec.type,
optional: spec.optional,
visibility: MemberVisibility.Private,
docs: spec.docs,
});
this.addProperty({
name: spec.name,
type: spec.type,
optional: spec.optional,
docs: spec.docs,
getterBody: Block.with(stmt.ret($this[backingFieldName])),
setterBody: (value: Expression) => Block.with(
CDK_CORE.traceProperty($this.node, expr.lit(prop.cfnName)),
stmt.assign($this[backingFieldName], value),
),
});
}
}

// Copy properties onto class and props type
Expand Down Expand Up @@ -745,8 +769,8 @@ export class ResourceClass extends ClassType implements Referenceable {

init.addBody(
// Props
...this.decider.classProperties.map(({ propertySpec: { name }, initializer }) =>
stmt.assign($this[name], initializer(props)),
...this.decider.classProperties.map(({ propertySpec: { name, immutable }, initializer }) =>
stmt.assign($this[immutable ? name : `_${name}`], initializer(props)),
),
);

Expand Down
8 changes: 8 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export class ResourceDecider {
immutable: false,
docs: this.defaultClassPropDocs(cfnName, prop),
},
cfnName,
initializer: resolverResult.resolver,
cfnValueToRender: { [resolverResult.name]: $this[resolverResult.name] },
});
Expand Down Expand Up @@ -166,6 +167,7 @@ export class ResourceDecider {
summary: 'Tag Manager which manages the tags for this resource',
},
},
cfnName,
initializer: (props: Expression) =>
new CDK_CORE.TagManager(
this.tagManagerVariant(variant),
Expand All @@ -184,6 +186,7 @@ export class ResourceDecider {
optional: true, // Tags are never required
docs: this.defaultClassPropDocs(cfnName, prop),
},
cfnName,
initializer: (props: Expression) => $E(props)[originalName],
cfnValueToRender: {}, // Gets rendered as part of the TagManager above
},
Expand Down Expand Up @@ -221,6 +224,7 @@ export class ResourceDecider {
summary: 'Tag Manager which manages the tags for this resource',
},
},
cfnName,
initializer: (_: Expression) =>
new CDK_CORE.TagManager(
this.tagManagerVariant(variant),
Expand All @@ -239,6 +243,7 @@ export class ResourceDecider {
optional: true, // Tags are never required
docs: this.defaultClassPropDocs(cfnName, prop),
},
cfnName,
initializer: (props: Expression) => $E(props)[originalName],
cfnValueToRender: {}, // Gets rendered as part of the TagManager above
},
Expand Down Expand Up @@ -364,6 +369,9 @@ export interface PropsProperty {
export interface ClassProperty {
readonly propertySpec: PropertySpec;

/** The original CloudFormation property name */
readonly cfnName: string;

/** Given the name of the props value, produce the member value */
readonly initializer: (props: Expression) => Expression;

Expand Down
37 changes: 37 additions & 0 deletions tools/@aws-cdk/spec2cdk/test/resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,43 @@ test('CFN reference identifier of same length as CC-API identifier aliases field
);
});

describe('Source tracing', () => {
test('generates setter for each property, with a call to traceProperty', () => {
givenResource({
...BASE_RESOURCE,
attributes: {
Arn: {
type: { type: 'string' },
documentation: 'The ARN of this resource',
},
},
primaryIdentifier: ['Id'],
cfnRefIdentifier: ['Arn'],
properties: {
Id: {
type: { type: 'string' },
},
Foo: {
type: { type: 'string' },
},
},
});

// THEN
const rendered = renderResource();

expect(rendered.resources).toContainCode(`public set id(value: string | undefined) {
cdk.traceProperty(this.node, "Id");
this._id = value;
}`);

expect(rendered.resources).toContainCode(`public set foo(value: string | undefined) {
cdk.traceProperty(this.node, "Foo");
this._foo = value;
}`);
});
});

function givenResource(res: Plain<Resource>) {
db.link('hasResource', service, db.allocate('resource', res));
}
Expand Down
Loading