Skip to content

Commit 6004a17

Browse files
authored
fix(cli): hotswap cannot evaluate nested stacks within nested stacks (#28080)
This line https://github.com/aws/aws-cdk/blob/25ee8ef61d6b33628923a0fee4c042a0f0f2441e/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts#L390 doesn't actually finds the nested stack in the tree but only at the top level which causes references made to second (or more) level nested stacks return undefined. This causes hotswap to fail Closes #<issue number here>. #28077 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
2 parents 645d4e9 + 4c6b628 commit 6004a17

File tree

3 files changed

+98
-10
lines changed

3 files changed

+98
-10
lines changed

packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ export class EvaluateCloudFormationTemplate {
387387

388388
if (foundResource.ResourceType == 'AWS::CloudFormation::Stack' && attribute?.startsWith('Outputs.')) {
389389
// need to resolve attributes from another stack's Output section
390-
const dependantStackName = this.nestedStackNames[logicalId]?.nestedStackPhysicalName;
390+
const dependantStackName = this.findNestedStack(logicalId, this.nestedStackNames);
391391
if (!dependantStackName) {
392392
//this is a newly created nested stack and cannot be hotswapped
393393
return undefined;
@@ -406,6 +406,19 @@ export class EvaluateCloudFormationTemplate {
406406
return this.formatResourceAttribute(foundResource, attribute);
407407
}
408408

409+
private findNestedStack(logicalId: string, nestedStackNames: {
410+
[nestedStackLogicalId: string]: NestedStackNames;
411+
}): string | undefined {
412+
for (const [nestedStackLogicalId, { nestedChildStackNames, nestedStackPhysicalName }] of Object.entries(nestedStackNames)) {
413+
if (nestedStackLogicalId === logicalId) {
414+
return nestedStackPhysicalName;
415+
}
416+
const checkInNestedChildStacks = this.findNestedStack(logicalId, nestedChildStackNames);
417+
if (checkInNestedChildStacks) return checkInNestedChildStacks;
418+
}
419+
return undefined;
420+
}
421+
409422
private formatResourceAttribute(resource: AWS.CloudFormation.StackResourceSummary, attribute: string | undefined): string | undefined {
410423
const physicalId = resource.PhysicalResourceId;
411424

packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -826,10 +826,12 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
826826
});
827827
});
828828

829-
test('can hotswap a lambda function in a 1-level nested stack with dependency on a output of sibling stack', async () => {
830-
// GIVEN: RootStack has two child stacks `NestedLambdaStack` and `NestedSiblingStack`. `NestedLambdaStack`
831-
// takes two parameters s3Key and s3Bucket and use them for a Lambda function.
832-
// RootStack resolves s3Bucket from a root template parameter and s3Key through output of `NestedSiblingStack`
829+
test('can hotswap a lambda function in a 2-level nested stack with dependency on a output of 2nd level sibling stack', async () => {
830+
// GIVEN: RootStack has one child stack `FirstLevelRootStack` which further has two child stacks
831+
// `NestedLambdaStack` and `NestedSiblingStack`. `NestedLambdaStack` takes two parameters s3Key
832+
// and s3Bucket and use them for a Lambda function.
833+
// RootStack resolves s3Bucket from a root template parameter and passed to FirstLevelRootStack which
834+
// resolves s3Key through output of `NestedSiblingStack`
833835
hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('RootStack');
834836
mockUpdateLambdaCode = jest.fn().mockReturnValue({});
835837
hotswapMockSdkProvider.stubLambda({
@@ -838,6 +840,34 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
838840

839841
const rootStack = testStack({
840842
stackName: 'RootStack',
843+
template: {
844+
Resources: {
845+
FirstLevelRootStack: {
846+
Type: 'AWS::CloudFormation::Stack',
847+
Properties: {
848+
TemplateURL: 'https://www.magic-url.com',
849+
Parameters: {
850+
S3BucketParam: {
851+
Ref: 'S3BucketParam',
852+
},
853+
},
854+
},
855+
Metadata: {
856+
'aws:asset:path': 'one-stack-with-two-nested-stacks-stack.template.json',
857+
},
858+
},
859+
},
860+
Parameters: {
861+
S3BucketParam: {
862+
Type: 'String',
863+
Description: 'S3 bucket for asset',
864+
},
865+
},
866+
},
867+
});
868+
869+
const firstLevelRootStack = testStack({
870+
stackName: 'FirstLevelRootStack',
841871
template: {
842872
Resources: {
843873
NestedLambdaStack: {
@@ -869,11 +899,11 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
869899
'aws:asset:path': 'one-output-stack.nested.template.json',
870900
},
871901
},
872-
Parameters: {
873-
S3BucketParam: {
874-
Type: 'String',
875-
Description: 'S3 bucket for asset',
876-
},
902+
},
903+
Parameters: {
904+
S3BucketParam: {
905+
Type: 'String',
906+
Description: 'S3 bucket for asset',
877907
},
878908
},
879909
},
@@ -913,10 +943,17 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
913943
});
914944

915945
setup.addTemplateToCloudFormationLookupMock(rootStack);
946+
setup.addTemplateToCloudFormationLookupMock(firstLevelRootStack);
916947
setup.addTemplateToCloudFormationLookupMock(nestedLambdaStack);
917948
setup.addTemplateToCloudFormationLookupMock(nestedSiblingStack);
918949

919950
setup.pushNestedStackResourceSummaries('RootStack',
951+
setup.stackSummaryOf('FirstLevelRootStack', 'AWS::CloudFormation::Stack',
952+
'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/FirstLevelRootStack/abcd',
953+
),
954+
);
955+
956+
setup.pushNestedStackResourceSummaries('FirstLevelRootStack',
920957
setup.stackSummaryOf('NestedLambdaStack', 'AWS::CloudFormation::Stack',
921958
'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedLambdaStack/abcd',
922959
),
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"Resources": {
3+
"NestedLambdaStack": {
4+
"Type": "AWS::CloudFormation::Stack",
5+
"Properties": {
6+
"TemplateURL": "https://www.magic-url.com",
7+
"Parameters": {
8+
"referenceToS3BucketParam": {
9+
"Ref": "S3BucketParam"
10+
},
11+
"referenceToS3StackKeyOutput": {
12+
"Fn::GetAtt": [
13+
"NestedSiblingStack",
14+
"Outputs.NestedOutput"
15+
]
16+
}
17+
}
18+
},
19+
"Metadata": {
20+
"aws:asset:path": "one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json"
21+
}
22+
},
23+
"NestedSiblingStack": {
24+
"Type": "AWS::CloudFormation::Stack",
25+
"Properties": {
26+
"TemplateURL": "https://www.magic-url.com"
27+
},
28+
"Metadata": {
29+
"aws:asset:path": "one-output-stack.nested.template.json"
30+
}
31+
}
32+
},
33+
"Parameters": {
34+
"S3BucketParam": {
35+
"Type": "String"
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)