Skip to content
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
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 propertyAssignment 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 propertyAssignmentEntry = metadata['/Default/MyTopic'].find((e: any) => e.type === 'aws:cdk:propertyAssignment');

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

test('Metadata does not contain propertyAssignment 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 propertyAssignmentEntry = metadata['/Default/MyTopic'].find((e: any) => e.type === 'aws:cdk:propertyAssignment');

expect(propertyAssignmentEntry).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);
}
23 changes: 23 additions & 0 deletions packages/aws-cdk-lib/core/lib/stack-trace.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Node } from 'constructs';
import { debugModeEnabled } from './debug';

/**
Expand Down Expand Up @@ -190,3 +191,25 @@ interface CallSite {
fileName: string;
sourceLocation: string;
}

/**
* Records a metadata entry on a construct node to trace a property assignment.
*
* When debug mode is enabled (via the `CDK_DEBUG` environment variable),
* this attaches `aws:cdk:propertyAssignment` metadata to the given node,
* including a stack trace pointing back to the caller. This is useful for
* diagnosing where a particular property value was set during synthesis.
*
* This is a no-op when debug mode is not enabled.
*
* @param node the construct node to attach the metadata to.
* @param propertyName the name of the property being assigned.
*/
export function traceProperty(node: Node, propertyName: string) {
if (debugModeEnabled()) {
node.addMetadata('aws:cdk:propertyAssignment', {
propertyName,
stackTrace: captureStackTrace(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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`L1 property mixin for a standard-issue resource 1`] = `
"/* eslint-disable prettier/prettier, @stylistic/max-len */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`can codegen deprecated service 1`] = `
"/* eslint-disable prettier/prettier, @stylistic/max-len */
Expand Down Expand Up @@ -54,7 +54,7 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, IR
/**
* The identifier of the resource.
*/
public id?: string;
private _id?: string;
/**
* Create a new \`AWS::Some::Resource\`.
Expand All @@ -69,7 +69,7 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, IR
properties: props
});
this.id = props.id;
this._id = props.id;
}
public get resourceRef(): ResourceReference {
Expand All @@ -78,6 +78,20 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, IR
};
}
/**
* The identifier of the resource.
*/
public get id(): string | undefined {
return this._id;
}
/**
* The identifier of the resource.
*/
public set id(value: string | undefined) {
cdk.traceProperty(this.node, "Id");
this._id = value;
}
protected get cfnProperties(): Record<string, any> {
return {
id: this.id
Expand Down
Loading
Loading