|
1 | 1 | 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'; |
3 | 8 | import { ToolkitError } from '../../toolkit/toolkit-error'; |
4 | 9 |
|
5 | 10 | /** |
6 | 11 | * Generates a list of stack definitions to be sent to the CloudFormation API |
7 | 12 | * by applying each mapping to the corresponding stack template(s). |
8 | 13 | */ |
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]), |
20 | 21 | ); |
| 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]; |
21 | 32 |
|
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]; |
34 | 35 | } |
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]; |
36 | 46 |
|
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 | + } |
40 | 57 | }); |
41 | 58 |
|
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)) { |
44 | 80 | 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 | + ); |
46 | 84 | } |
47 | 85 | } |
48 | 86 |
|
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 | + })); |
53 | 98 | } |
0 commit comments