Skip to content

Commit

Permalink
fix(cli): hotswap cannot evaluate nested stacks within nested stacks
Browse files Browse the repository at this point in the history
  • Loading branch information
Amplifiyer authored and rix0rrr committed Nov 21, 2023
1 parent c6471f2 commit 8fa6bdd
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 10 deletions.
15 changes: 14 additions & 1 deletion packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export class EvaluateCloudFormationTemplate {

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

private findNestedStack(logicalId: string, nestedStackNames: {
[nestedStackLogicalId: string]: NestedStackNames;
}): string | undefined {
for (const [nestedStackLogicalId, { nestedChildStackNames, nestedStackPhysicalName }] of Object.entries(nestedStackNames)) {
if (nestedStackLogicalId === logicalId) {
return nestedStackPhysicalName;
}
const checkInNestedChildStacks = this.findNestedStack(logicalId, nestedChildStackNames);
if (checkInNestedChildStacks) return checkInNestedChildStacks;
}
return undefined;
}

private formatResourceAttribute(resource: AWS.CloudFormation.StackResourceSummary, attribute: string | undefined): string | undefined {
const physicalId = resource.PhysicalResourceId;

Expand Down
55 changes: 46 additions & 9 deletions packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,10 +826,12 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
});
});

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

const rootStack = testStack({
stackName: 'RootStack',
template: {
Resources: {
FirstLevelRootStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
Parameters: {
S3BucketParam: {
Ref: 'S3BucketParam',
},
},
},
Metadata: {
'aws:asset:path': 'one-stack-with-two-nested-stacks-stack.template.json',
},
},
},
Parameters: {
S3BucketParam: {
Type: 'String',
Description: 'S3 bucket for asset',
},
},
},
});

const firstLevelRootStack = testStack({
stackName: 'FirstLevelRootStack',
template: {
Resources: {
NestedLambdaStack: {
Expand Down Expand Up @@ -869,11 +899,11 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
'aws:asset:path': 'one-output-stack.nested.template.json',
},
},
Parameters: {
S3BucketParam: {
Type: 'String',
Description: 'S3 bucket for asset',
},
},
Parameters: {
S3BucketParam: {
Type: 'String',
Description: 'S3 bucket for asset',
},
},
},
Expand Down Expand Up @@ -913,10 +943,17 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
});

setup.addTemplateToCloudFormationLookupMock(rootStack);
setup.addTemplateToCloudFormationLookupMock(firstLevelRootStack);
setup.addTemplateToCloudFormationLookupMock(nestedLambdaStack);
setup.addTemplateToCloudFormationLookupMock(nestedSiblingStack);

setup.pushNestedStackResourceSummaries('RootStack',
setup.stackSummaryOf('FirstLevelRootStack', 'AWS::CloudFormation::Stack',
'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/FirstLevelRootStack/abcd',
),
);

setup.pushNestedStackResourceSummaries('FirstLevelRootStack',
setup.stackSummaryOf('NestedLambdaStack', 'AWS::CloudFormation::Stack',
'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedLambdaStack/abcd',
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"Resources": {
"NestedLambdaStack": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "https://www.magic-url.com",
"Parameters": {
"referenceToS3BucketParam": {
"Ref": "S3BucketParam"
},
"referenceToS3StackKeyOutput": {
"Fn::GetAtt": [
"NestedSiblingStack",
"Outputs.NestedOutput"
]
}
}
},
"Metadata": {
"aws:asset:path": "one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json"
}
},
"NestedSiblingStack": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "https://www.magic-url.com"
},
"Metadata": {
"aws:asset:path": "one-output-stack.nested.template.json"
}
}
},
"Parameters": {
"S3BucketParam": {
"Type": "String"
}
}
}

0 comments on commit 8fa6bdd

Please sign in to comment.