Skip to content

Commit bcc5ce0

Browse files
committed
feat: generate stack definitions for refactoring
1 parent e4c6a17 commit bcc5ce0

File tree

2 files changed

+329
-5
lines changed

2 files changed

+329
-5
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { StackDefinition } from '@aws-sdk/client-cloudformation';
2+
import type { CloudFormationStack, ResourceMapping } from './cloudformation';
3+
4+
/**
5+
* Generates a list of stack definitions to be sent to the CloudFormation API
6+
* by applying each mapping to the corresponding stack template(s).
7+
*/
8+
export function generateStackDefinitions(mappings: ResourceMapping[], deployedStacks: CloudFormationStack[]): StackDefinition[] {
9+
const deployedTemplates = Object.fromEntries(
10+
deployedStacks
11+
.filter((s) =>
12+
mappings.some(
13+
(m) =>
14+
// We only care about stacks that are part of the mappings
15+
m.source.stack.stackName === s.stackName || m.destination.stack.stackName === s.stackName,
16+
),
17+
)
18+
.map((s) => [s.stackName, JSON.parse(JSON.stringify(s.template))]),
19+
);
20+
21+
mappings.forEach((mapping) => {
22+
const sourceStackName = mapping.source.stack.stackName;
23+
const sourceLogicalId = mapping.source.logicalResourceId;
24+
const sourceTemplate = deployedTemplates[sourceStackName];
25+
26+
const destinationStackName = mapping.destination.stack.stackName;
27+
const destinationLogicalId = mapping.destination.logicalResourceId;
28+
if (deployedTemplates[destinationStackName] == null) {
29+
// The API doesn't allow anything in the template other than the resources
30+
// that are part of the mappings. So we need to create an empty template
31+
// to start adding resources to.
32+
deployedTemplates[destinationStackName] = { Resources: {} };
33+
}
34+
const destinationTemplate = deployedTemplates[destinationStackName];
35+
36+
// Do the move
37+
destinationTemplate.Resources[destinationLogicalId] = sourceTemplate.Resources[sourceLogicalId];
38+
delete sourceTemplate.Resources[sourceLogicalId];
39+
});
40+
41+
return Object.entries(deployedTemplates).map(([stackName, template]) => ({
42+
StackName: stackName,
43+
TemplateBody: JSON.stringify(template),
44+
}));
45+
}

packages/@aws-cdk/toolkit-lib/test/api/refactoring/refactoring.test.ts

Lines changed: 284 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@ import {
1717
resourceMappings,
1818
resourceMovements,
1919
} from '../../../lib/api/refactoring';
20-
import type {
21-
ResourceLocation,
22-
ResourceMapping,
23-
CloudFormationStack,
24-
} from '../../../lib/api/refactoring/cloudformation';
20+
import type { CloudFormationStack } from '../../../lib/api/refactoring/cloudformation';
21+
import { ResourceLocation, ResourceMapping } from '../../../lib/api/refactoring/cloudformation';
2522
import { computeResourceDigests } from '../../../lib/api/refactoring/digest';
23+
import { generateStackDefinitions } from '../../../lib/api/refactoring/execution';
2624
import { mockCloudFormationClient, MockSdkProvider } from '../../_helpers/mock-sdk';
2725

2826
const cloudFormationClient = mockCloudFormationClient;
@@ -1380,6 +1378,287 @@ describe('environment grouping', () => {
13801378
});
13811379
});
13821380

1381+
describe(generateStackDefinitions, () => {
1382+
const environment = {
1383+
name: 'test',
1384+
account: '333333333333',
1385+
region: 'us-east-1',
1386+
};
1387+
1388+
test('renames a resource within the same stack', () => {
1389+
const stack: CloudFormationStack = {
1390+
environment: environment,
1391+
stackName: 'Foo',
1392+
template: {
1393+
Resources: {
1394+
Bucket1: {
1395+
Type: 'AWS::S3::Bucket',
1396+
},
1397+
NotInvolved: {
1398+
Type: 'AWS::X::Y',
1399+
},
1400+
},
1401+
},
1402+
};
1403+
1404+
const mappings: ResourceMapping[] = [
1405+
new ResourceMapping(new ResourceLocation(stack, 'Bucket1'), new ResourceLocation(stack, 'Bucket2')),
1406+
];
1407+
1408+
const result = generateStackDefinitions(mappings, [stack]);
1409+
expect(result).toEqual([
1410+
{
1411+
StackName: 'Foo',
1412+
TemplateBody: JSON.stringify({
1413+
Resources: {
1414+
// Not involved in the refactor, but still part of the
1415+
// original template. Should be included.
1416+
NotInvolved: {
1417+
Type: 'AWS::X::Y',
1418+
},
1419+
Bucket2: {
1420+
Type: 'AWS::S3::Bucket',
1421+
},
1422+
},
1423+
}),
1424+
},
1425+
]);
1426+
});
1427+
1428+
test('moves a resource to another stack that has already been deployed', () => {
1429+
const stack1: CloudFormationStack = {
1430+
environment,
1431+
stackName: 'Stack1',
1432+
template: {
1433+
Resources: {
1434+
Bucket1: {
1435+
Type: 'AWS::S3::Bucket',
1436+
},
1437+
A: {
1438+
Type: 'AWS::A::A',
1439+
},
1440+
},
1441+
},
1442+
};
1443+
1444+
const stack2: CloudFormationStack = {
1445+
environment,
1446+
stackName: 'Stack2',
1447+
template: {
1448+
Resources: {
1449+
B: {
1450+
Type: 'AWS::B::B',
1451+
},
1452+
},
1453+
},
1454+
};
1455+
1456+
const mappings: ResourceMapping[] = [
1457+
new ResourceMapping(new ResourceLocation(stack1, 'Bucket1'), new ResourceLocation(stack2, 'Bucket2')),
1458+
];
1459+
1460+
const result = generateStackDefinitions(mappings, [stack1, stack2]);
1461+
expect(result).toEqual([
1462+
{
1463+
StackName: 'Stack1',
1464+
TemplateBody: JSON.stringify({
1465+
Resources: {
1466+
// Wasn't touched by the refactor
1467+
A: {
1468+
Type: 'AWS::A::A',
1469+
},
1470+
1471+
// Bucket1 doesn't exist anymore
1472+
},
1473+
}),
1474+
},
1475+
{
1476+
StackName: 'Stack2',
1477+
TemplateBody: JSON.stringify({
1478+
Resources: {
1479+
// Wasn't touched by the refactor
1480+
B: {
1481+
Type: 'AWS::B::B',
1482+
},
1483+
1484+
// Old Bucket1 is now Bucket2 here
1485+
Bucket2: {
1486+
Type: 'AWS::S3::Bucket',
1487+
},
1488+
},
1489+
}),
1490+
},
1491+
]);
1492+
});
1493+
1494+
test('moves a resource to another stack that has not been deployed', () => {
1495+
const stack1: CloudFormationStack = {
1496+
environment,
1497+
stackName: 'Stack1',
1498+
template: {
1499+
Resources: {
1500+
Bucket1: {
1501+
Type: 'AWS::S3::Bucket',
1502+
},
1503+
A: {
1504+
Type: 'AWS::A::A',
1505+
},
1506+
},
1507+
},
1508+
};
1509+
1510+
const stack2: CloudFormationStack = {
1511+
environment,
1512+
stackName: 'Stack2',
1513+
template: {
1514+
Resources: {
1515+
B: {
1516+
Type: 'AWS::B::B',
1517+
},
1518+
},
1519+
},
1520+
};
1521+
1522+
const mappings: ResourceMapping[] = [
1523+
new ResourceMapping(new ResourceLocation(stack1, 'Bucket1'), new ResourceLocation(stack2, 'Bucket2')),
1524+
];
1525+
1526+
const result = generateStackDefinitions(mappings, [stack1]);
1527+
expect(result).toEqual([
1528+
{
1529+
StackName: 'Stack1',
1530+
TemplateBody: JSON.stringify({
1531+
Resources: {
1532+
// Wasn't touched by the refactor
1533+
A: {
1534+
Type: 'AWS::A::A',
1535+
},
1536+
1537+
// Bucket1 doesn't exist anymore
1538+
},
1539+
}),
1540+
},
1541+
{
1542+
StackName: 'Stack2',
1543+
TemplateBody: JSON.stringify({
1544+
Resources: {
1545+
// Old Bucket1 is now Bucket2 here
1546+
Bucket2: {
1547+
Type: 'AWS::S3::Bucket',
1548+
},
1549+
},
1550+
}),
1551+
},
1552+
]);
1553+
});
1554+
1555+
test('multiple mappings', () => {
1556+
const stack1: CloudFormationStack = {
1557+
environment,
1558+
stackName: 'Stack1',
1559+
template: {
1560+
Resources: {
1561+
Bucket1: {
1562+
Type: 'AWS::S3::Bucket',
1563+
},
1564+
Bucket2: {
1565+
Type: 'AWS::S3::Bucket',
1566+
},
1567+
},
1568+
},
1569+
};
1570+
1571+
const stack2: CloudFormationStack = {
1572+
environment,
1573+
stackName: 'Stack2',
1574+
template: {
1575+
Resources: {
1576+
Bucket3: {
1577+
Type: 'AWS::S3::Bucket',
1578+
},
1579+
},
1580+
},
1581+
};
1582+
1583+
const mappings: ResourceMapping[] = [
1584+
new ResourceMapping(new ResourceLocation(stack1, 'Bucket1'), new ResourceLocation(stack2, 'Bucket4')),
1585+
new ResourceMapping(new ResourceLocation(stack1, 'Bucket2'), new ResourceLocation(stack2, 'Bucket5')),
1586+
new ResourceMapping(new ResourceLocation(stack2, 'Bucket3'), new ResourceLocation(stack1, 'Bucket6')),
1587+
];
1588+
1589+
const result = generateStackDefinitions(mappings, [stack1, stack2]);
1590+
expect(result).toEqual([
1591+
{
1592+
StackName: 'Stack1',
1593+
TemplateBody: JSON.stringify({
1594+
Resources: {
1595+
Bucket6: {
1596+
Type: 'AWS::S3::Bucket',
1597+
},
1598+
},
1599+
}),
1600+
},
1601+
{
1602+
StackName: 'Stack2',
1603+
TemplateBody: JSON.stringify({
1604+
Resources: {
1605+
Bucket4: {
1606+
Type: 'AWS::S3::Bucket',
1607+
},
1608+
Bucket5: {
1609+
Type: 'AWS::S3::Bucket',
1610+
},
1611+
},
1612+
}),
1613+
},
1614+
]);
1615+
});
1616+
1617+
test('deployed stacks that are not in any mapping', () => {
1618+
const stack1: CloudFormationStack = {
1619+
environment,
1620+
stackName: 'Stack1',
1621+
template: {
1622+
Resources: {
1623+
Bucket1: {
1624+
Type: 'AWS::S3::Bucket',
1625+
},
1626+
},
1627+
},
1628+
};
1629+
1630+
const stack2: CloudFormationStack = {
1631+
environment,
1632+
stackName: 'Stack2',
1633+
template: {
1634+
Resources: {
1635+
Bucket2: {
1636+
Type: 'AWS::S3::Bucket',
1637+
},
1638+
},
1639+
},
1640+
};
1641+
1642+
const mappings: ResourceMapping[] = [
1643+
new ResourceMapping(new ResourceLocation(stack1, 'Bucket1'), new ResourceLocation(stack1, 'Bucket3')),
1644+
];
1645+
1646+
const result = generateStackDefinitions(mappings, [stack1, stack2]);
1647+
expect(result).toEqual([
1648+
{
1649+
StackName: 'Stack1',
1650+
TemplateBody: JSON.stringify({
1651+
Resources: {
1652+
Bucket3: {
1653+
Type: 'AWS::S3::Bucket',
1654+
},
1655+
},
1656+
}),
1657+
},
1658+
]);
1659+
});
1660+
});
1661+
13831662
function toCfnMapping(m: ResourceMapping): CfnResourceMapping {
13841663
return {
13851664
Source: toCfnLocation(m.source),

0 commit comments

Comments
 (0)