Skip to content

Commit c4fdf1a

Browse files
committed
fix(deployment): resolve AWS::NotificationARNs pseudo-parameter to empty in Fn::Sub/Ref
cdkd's intrinsic resolver returned `undefined` for the AWS::NotificationARNs pseudo-parameter. In an Fn::Sub body that left the literal placeholder `${AWS::NotificationARNs}` in the output (the pseudo branch is skipped on `undefined`, and the subsequent Ref attempt throws + keeps the placeholder), diverging from CloudFormation. cdkd has no stack-notification-ARN concept (a cdkd deploy never sets SNS notification ARNs on a stack), so the list is always empty. CloudFormation resolves an empty AWS::NotificationARNs list to an empty string in an Fn::Sub / Ref string context, so the resolver now returns '' (empty string) for AWS::NotificationARNs in both Fn::Sub and bare Ref contexts. - src/deployment/intrinsic-function-resolver.ts: return '' instead of undefined - tests/unit/deployment/intrinsic-functions.test.ts: add a describe block asserting Fn::Sub('${AWS::NotificationARNs}') and Ref: AWS::NotificationARNs both resolve to '' - tests/integration/intrinsics-torture: update the verify.sh expected value from `notif=undefined` to `notif=` (the prior expectation pinned a non-existent "undefined" behavior); update the stack/README/changelog notes to describe the CFn-parity empty-string resolution
1 parent 2c21bef commit c4fdf1a

5 files changed

Lines changed: 65 additions & 18 deletions

File tree

src/deployment/intrinsic-function-resolver.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2096,7 +2096,13 @@ export class IntrinsicFunctionResolver {
20962096
return 'amazonaws.com';
20972097

20982098
case 'AWS::NotificationARNs':
2099-
return undefined;
2099+
// cdkd has no stack-notification-ARN concept — a cdkd deploy never
2100+
// sets SNS notification ARNs on a stack — so the list is always
2101+
// empty. CloudFormation resolves an empty AWS::NotificationARNs list
2102+
// to an empty string in an Fn::Sub / Ref string context, so returning
2103+
// '' (not undefined) matches CFn parity and ensures the pseudo
2104+
// parameter is substituted rather than left as a literal placeholder.
2105+
return '';
21002106

21012107
case 'AWS::NoValue':
21022108
// Return special symbol to indicate property should be omitted

tests/integration/intrinsics-torture/README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,17 @@ This goes **beyond** the existing
4141
| `base64` | `Fn::Base64('cdkd-intrinsics-torture')` | `Y2RrZC1pbnRyaW5zaWNzLXRvcnR1cmU=` |
4242
| `split-select-join` | nested `Fn::Join` of three `Fn::Select`-of-`Fn::Split` picks | `a\|c\|e` |
4343
| `nested-sub` | two-arg `Fn::Sub`: literal-map var (nested `Fn::Join`) + `${AWS::Region}` + `${TortureQueue.Arn}` GetAtt | `label=cdkd-torture-sub;region=<r>;queueArn=arn:<p>:sqs:<r>:<acct>:...` |
44-
| `pseudo` | `Fn::Sub` over ALL pseudo-params: `${AWS::AccountId}` / `${AWS::Region}` / `${AWS::Partition}` / `${AWS::StackName}` / `${AWS::URLSuffix}` / `${AWS::NotificationARNs}` | `account=<acct>;region=<r>;partition=aws;stack=<stack>;urlsuffix=amazonaws.com;notif=undefined` |
44+
| `pseudo` | `Fn::Sub` over ALL pseudo-params: `${AWS::AccountId}` / `${AWS::Region}` / `${AWS::Partition}` / `${AWS::StackName}` / `${AWS::URLSuffix}` / `${AWS::NotificationARNs}` | `account=<acct>;region=<r>;partition=aws;stack=<stack>;urlsuffix=amazonaws.com;notif=` |
4545
| `topic-ref-sub` | `Fn::Sub` with pseudo params + a `Ref` to the SNS topic | `arn-prefix=arn:<p>:sns:<r>:<acct>;topicRef=arn:<p>:sns:<r>:<acct>:...` |
4646

4747
### Note on `AWS::NotificationARNs`
4848

49-
cdkd resolves the `AWS::NotificationARNs` list pseudo-parameter to `undefined`
50-
(there is no CloudFormation notification-ARN list in cdkd's
51-
CloudFormation-free model). Inside `Fn::Sub` that stringifies to the literal
52-
`undefined`. The `pseudo` assertion **pins this documented behavior** — a
53-
regression that changed it (to an empty string, or a crash) would flip the
54-
assertion. This is intentional: the test exists to catch divergence, including
55-
in cdkd's own deliberate semantics.
49+
cdkd has no CloudFormation notification-ARN list in its CloudFormation-free
50+
model, so `AWS::NotificationARNs` is always an empty list. Matching
51+
CloudFormation's behavior for an empty list, it resolves to an **empty string**
52+
inside `Fn::Sub` (so `notif=` has nothing after the `=`). The `pseudo`
53+
assertion **pins this CFn-parity behavior** — a regression that left the literal
54+
`${AWS::NotificationARNs}` placeholder, or crashed, would flip the assertion.
5655

5756
## Resources
5857

tests/integration/intrinsics-torture/lib/intrinsics-torture-stack.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,10 @@ export class IntrinsicsTortureStack extends cdk.Stack {
199199
// -----------------------------------------------------------------
200200
// 7. ALL pseudo-parameters, each fed through Fn::Sub into a parameter
201201
// value so verify.sh can read back the resolved concrete value.
202-
// AWS::NotificationARNs is a LIST pseudo param that cdkd resolves to
203-
// `undefined` (there is no notification ARN list in cdkd's
204-
// CloudFormation-free model); inside Fn::Sub that stringifies to the
205-
// literal "undefined". verify.sh asserts cdkd's documented behavior.
202+
// AWS::NotificationARNs is a LIST pseudo param that is always empty in
203+
// cdkd's CloudFormation-free model (there is no notification ARN list).
204+
// Matching CloudFormation, an empty list resolves to an EMPTY STRING
205+
// inside Fn::Sub, so `notif=` is empty. verify.sh asserts this.
206206
// -----------------------------------------------------------------
207207
intrinsicParam(
208208
'PseudoParam',

tests/integration/intrinsics-torture/verify.sh

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,13 @@ case "${NESTED_SUB}" in
197197
esac
198198

199199
# -- 7. ALL pseudo-parameters via Fn::Sub --
200-
# cdkd resolves AWS::NotificationARNs to `undefined` (no CFn notification ARN
201-
# list in cdkd's model); inside Fn::Sub it stringifies to the literal
202-
# "undefined". This pins cdkd's DOCUMENTED behavior — a regression that
203-
# changed it (e.g. to empty string, or a crash) would flip this assertion.
204-
PSEUDO_EXPECTED="account=${ACCOUNT_ID};region=${REGION};partition=${PARTITION};stack=${STACK};urlsuffix=amazonaws.com;notif=undefined"
200+
# cdkd has no stack-notification-ARN concept (no CFn notification ARN list in
201+
# cdkd's model), so AWS::NotificationARNs is always an empty list. Matching
202+
# CloudFormation, an empty AWS::NotificationARNs list resolves to an EMPTY
203+
# STRING inside an Fn::Sub body — so `notif=` (nothing after the `=`). A
204+
# regression that left the literal `${AWS::NotificationARNs}` placeholder
205+
# (or crashed) would flip this assertion.
206+
PSEUDO_EXPECTED="account=${ACCOUNT_ID};region=${REGION};partition=${PARTITION};stack=${STACK};urlsuffix=amazonaws.com;notif="
205207
assert_param "all pseudo-parameters (Fn::Sub)" "${PFX}/pseudo" "${PSEUDO_EXPECTED}"
206208

207209
# -- 8. Fn::Sub with pseudo params + a Ref to the SNS topic --

tests/unit/deployment/intrinsic-functions.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,46 @@ describe('IntrinsicFunctionResolver - Fn::Sub same-stack implicit Ref', () => {
14241424
});
14251425
});
14261426

1427+
describe('IntrinsicFunctionResolver - AWS::NotificationARNs pseudo parameter', () => {
1428+
// cdkd has no stack-notification-ARN concept, so AWS::NotificationARNs is
1429+
// always an empty list — which CloudFormation resolves to an empty string
1430+
// in an Fn::Sub / Ref string context. Before the fix it resolved to
1431+
// `undefined`, which left the literal `${AWS::NotificationARNs}` placeholder
1432+
// in an Fn::Sub body (the pseudo branch was skipped on `undefined`).
1433+
let resolver: IntrinsicFunctionResolver;
1434+
1435+
beforeEach(() => {
1436+
resolver = new IntrinsicFunctionResolver();
1437+
resetAccountInfoCache();
1438+
});
1439+
1440+
const context: ResolverContext = {
1441+
template: { Resources: {} },
1442+
resources: {},
1443+
};
1444+
1445+
it('substitutes ${AWS::NotificationARNs} to an empty string in Fn::Sub', async () => {
1446+
const result = await resolver.resolve(
1447+
{ 'Fn::Sub': '${AWS::NotificationARNs}' },
1448+
context
1449+
);
1450+
expect(result).toBe('');
1451+
});
1452+
1453+
it('substitutes ${AWS::NotificationARNs} embedded in a surrounding Fn::Sub string', async () => {
1454+
const result = await resolver.resolve(
1455+
{ 'Fn::Sub': 'notif=${AWS::NotificationARNs};done' },
1456+
context
1457+
);
1458+
expect(result).toBe('notif=;done');
1459+
});
1460+
1461+
it('resolves a bare Ref: AWS::NotificationARNs to an empty string', async () => {
1462+
const result = await resolver.resolve({ Ref: 'AWS::NotificationARNs' }, context);
1463+
expect(result).toBe('');
1464+
});
1465+
});
1466+
14271467
describe('IntrinsicFunctionResolver - nested attribute path fallback (Issue #381)', () => {
14281468
let resolver: IntrinsicFunctionResolver;
14291469

0 commit comments

Comments
 (0)