Skip to content

Commit 29a724a

Browse files
authored
fix(toolkit-lib): stack definitions for refactor are not considering resource refrences (#547)
The `createStackRefactor` method, from the CloudFormation API, requires the client to pass not only the mappings, but also the templates in the state they are supposed to be after the refactor. Previously, this was done by iterating over the mappings (either computed by the toolkit or prescribed by the user), and using them as a list of instructions on how to modify the deployed stacks, moving resources around as necessary. This is a simple way to put the resources in their correct locations. But it becomes unwieldy when it comes to updating references between resources. Every time a resource is moved, all references to it -- and sometimes from it -- have to be updated as well. This PR changes implements a different algorithm: take all the synthesized templates as a starting point, and adjust them so that the final templates are equal to the deployed ones up to logical IDs and references. The result is what will be sent to CloudFormation. Specifically this involves: - Removing resources that exist locally but not remotely. - Adding resources that exist remotely but not locally. - Update the CDK construct path to the deployed value. (we will drop this once CloudFormation starts allowing changes in the construct path along with refactors). One case that is not yet covered is when physical IDs are set. Because we use the physical ID plus the type of the resource to identify it, users are free to change other properties, and we can still recognize that it's the same resource. Since we are simply taking the local resource as the basis and not updating anything other than the construct path, it's possible that it differs from the deployed one. This case will be handled in a following PR. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent f227c9b commit 29a724a

File tree

2 files changed

+1257
-75
lines changed

2 files changed

+1257
-75
lines changed
Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,98 @@
11
import type { StackDefinition } from '@aws-sdk/client-cloudformation';
2-
import type { CloudFormationStack, ResourceMapping } from './cloudformation';
2+
import type {
3+
CloudFormationResource,
4+
CloudFormationStack,
5+
CloudFormationTemplate,
6+
ResourceMapping,
7+
} from './cloudformation';
38
import { ToolkitError } from '../../toolkit/toolkit-error';
49

510
/**
611
* Generates a list of stack definitions to be sent to the CloudFormation API
712
* by applying each mapping to the corresponding stack template(s).
813
*/
9-
export function generateStackDefinitions(mappings: ResourceMapping[], deployedStacks: CloudFormationStack[]): StackDefinition[] {
10-
const templates = Object.fromEntries(
11-
deployedStacks
12-
.filter((s) =>
13-
mappings.some(
14-
(m) =>
15-
// We only care about stacks that are part of the mappings
16-
m.source.stack.stackName === s.stackName || m.destination.stack.stackName === s.stackName,
17-
),
18-
)
19-
.map((s) => [s.stackName, JSON.parse(JSON.stringify(s.template))]),
14+
export function generateStackDefinitions(
15+
mappings: ResourceMapping[],
16+
deployedStacks: CloudFormationStack[],
17+
localStacks: CloudFormationStack[],
18+
): StackDefinition[] {
19+
const localTemplates = Object.fromEntries(
20+
localStacks.map((s) => [s.stackName, JSON.parse(JSON.stringify(s.template)) as CloudFormationTemplate]),
2021
);
22+
const deployedTemplates = Object.fromEntries(
23+
deployedStacks.map((s) => [s.stackName, JSON.parse(JSON.stringify(s.template)) as CloudFormationTemplate]),
24+
);
25+
26+
// First, remove from the local templates any resources that are not in the deployed templates
27+
iterate(localTemplates, (stackName, logicalResourceId) => {
28+
const location = searchLocation(stackName, logicalResourceId, 'destination', 'source');
29+
30+
const deployedResource = deployedStacks.find((s) => s.stackName === location.stackName)?.template
31+
.Resources?.[location.logicalResourceId];
2132

22-
mappings.forEach((mapping) => {
23-
const sourceStackName = mapping.source.stack.stackName;
24-
const sourceLogicalId = mapping.source.logicalResourceId;
25-
const sourceTemplate = templates[sourceStackName];
26-
27-
const destinationStackName = mapping.destination.stack.stackName;
28-
const destinationLogicalId = mapping.destination.logicalResourceId;
29-
if (templates[destinationStackName] == null) {
30-
// The API doesn't allow anything in the template other than the resources
31-
// that are part of the mappings. So we need to create an empty template
32-
// to start adding resources to.
33-
templates[destinationStackName] = { Resources: {} };
33+
if (deployedResource == null) {
34+
delete localTemplates[stackName].Resources?.[logicalResourceId];
3435
}
35-
const destinationTemplate = templates[destinationStackName];
36+
});
37+
38+
// Now do the opposite: add to the local templates any resources that are in the deployed templates
39+
iterate(deployedTemplates, (stackName, logicalResourceId, deployedResource) => {
40+
const location = searchLocation(stackName, logicalResourceId, 'source', 'destination');
41+
42+
const resources = Object
43+
.entries(localTemplates)
44+
.find(([name, _]) => name === location.stackName)?.[1].Resources;
45+
const localResource = resources?.[location.logicalResourceId];
3646

37-
// Do the move
38-
destinationTemplate.Resources[destinationLogicalId] = sourceTemplate.Resources[sourceLogicalId];
39-
delete sourceTemplate.Resources[sourceLogicalId];
47+
if (localResource == null) {
48+
if (localTemplates[stackName]?.Resources) {
49+
localTemplates[stackName].Resources[logicalResourceId] = deployedResource;
50+
}
51+
} else {
52+
// This is temporary, until CloudFormation supports CDK construct path updates in the refactor API
53+
if (localResource.Metadata != null) {
54+
localResource.Metadata['aws:cdk:path'] = deployedResource.Metadata?.['aws:cdk:path'];
55+
}
56+
}
4057
});
4158

42-
// CloudFormation doesn't allow empty stacks
43-
for (const [stackName, template] of Object.entries(templates)) {
59+
function searchLocation(stackName: string, logicalResourceId: string, from: 'source' | 'destination', to: 'source' | 'destination') {
60+
const mapping = mappings.find(
61+
(m) => m[from].stack.stackName === stackName && m[from].logicalResourceId === logicalResourceId,
62+
);
63+
return mapping != null
64+
? { stackName: mapping[to].stack.stackName, logicalResourceId: mapping[to].logicalResourceId }
65+
: { stackName, logicalResourceId };
66+
}
67+
68+
function iterate(
69+
templates: Record<string, CloudFormationTemplate>,
70+
cb: (stackName: string, logicalResourceId: string, resource: CloudFormationResource) => void,
71+
) {
72+
Object.entries(templates).forEach(([stackName, template]) => {
73+
Object.entries(template.Resources ?? {}).forEach(([logicalResourceId, resource]) => {
74+
cb(stackName, logicalResourceId, resource);
75+
});
76+
});
77+
}
78+
79+
for (const [stackName, template] of Object.entries(localTemplates)) {
4480
if (Object.keys(template.Resources ?? {}).length === 0) {
45-
throw new ToolkitError(`Stack ${stackName} has no resources after refactor. You must add a resource to this stack. This resource can be a simple one, like a waitCondition resource type.`);
81+
throw new ToolkitError(
82+
`Stack ${stackName} has no resources after refactor. You must add a resource to this stack. This resource can be a simple one, like a waitCondition resource type.`,
83+
);
4684
}
4785
}
4886

49-
return Object.entries(templates).map(([stackName, template]) => ({
50-
StackName: stackName,
51-
TemplateBody: JSON.stringify(template),
52-
}));
87+
return Object.entries(localTemplates)
88+
.filter(([stackName, _]) =>
89+
mappings.some((m) => {
90+
// Only send templates for stacks that are part of the mappings
91+
return m.source.stack.stackName === stackName || m.destination.stack.stackName === stackName;
92+
}),
93+
)
94+
.map(([stackName, template]) => ({
95+
StackName: stackName,
96+
TemplateBody: JSON.stringify(template),
97+
}));
5398
}

0 commit comments

Comments
 (0)