Skip to content

Commit 49699d8

Browse files
otaviomacedogithub-actions
andauthored
fix(toolkit-lib): new mappingSource option to replace multiple mutual exclusive options in the refactor API (#559)
Introduce a new type, `MappingSource`, that allows the user to tell how they want the toolkit to get the mappings: computing it, using an explicitly provided one, or using the reverse of an explicitly provided one. This replaces three previous separate properties: `exclude`, `mappings` and `revert` with a single `mappingSource`. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions <[email protected]> Co-authored-by: github-actions <[email protected]>
1 parent c2f7af8 commit 49699d8

File tree

6 files changed

+117
-103
lines changed

6 files changed

+117
-103
lines changed

packages/@aws-cdk/cloudformation-diff/lib/mappings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ export function formatTypedMappings(stream: NodeJS.WritableStream, mappings: Typ
1818
const rows = mappings.map((m) => [m.type, m.sourcePath, m.destinationPath]);
1919

2020
const formatter = new Formatter(stream, {});
21+
formatter.print(`${env}:`);
2122
if (mappings.length > 0) {
22-
formatter.print(`${env}:`);
2323
formatter.print(chalk.green(formatTable(header.concat(rows), undefined)));
24-
formatter.print(' ');
2524
} else {
2625
formatter.print('Nothing to refactor.');
2726
}
27+
formatter.print(' ');
2828
}
2929

3030
export function formatAmbiguitySectionHeader(stream: NodeJS.WritableStream) {

packages/@aws-cdk/toolkit-lib/lib/actions/refactor/index.ts

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,66 @@
11
import type { StackSelector } from '../../api/cloud-assembly';
2+
import type { ExcludeList } from '../../api/refactoring';
3+
import { InMemoryExcludeList, NeverExclude } from '../../api/refactoring';
4+
5+
type MappingType = 'auto' | 'explicit';
6+
7+
/**
8+
* The source of the resource mappings to be used for refactoring.
9+
*/
10+
export class MappingSource {
11+
/**
12+
* The mapping will be automatically generated based on a comparison of
13+
* the deployed stacks and the local stacks.
14+
*
15+
* @param exclude A list of resource locations to exclude from the mapping.
16+
*/
17+
public static auto(exclude: string[] = []): MappingSource {
18+
const excludeList = new InMemoryExcludeList(exclude);
19+
return new MappingSource('auto', [], excludeList);
20+
}
21+
22+
/**
23+
* An explicitly provided list of mappings, which will be used for refactoring.
24+
*/
25+
public static explicit(groups: MappingGroup[]): MappingSource {
26+
return new MappingSource('explicit', groups, new NeverExclude());
27+
}
28+
29+
/**
30+
* An explicitly provided list of mappings, which will be used for refactoring,
31+
* but in reverse, that is, the source locations will become the destination
32+
* locations and vice versa.
33+
*/
34+
public static reverse(groups: MappingGroup[]): MappingSource {
35+
const reverseGroups = groups.map((group) => ({
36+
...group,
37+
resources: Object.fromEntries(Object.entries(group.resources).map(([src, dst]) => [dst, src])),
38+
}));
39+
40+
return MappingSource.explicit(reverseGroups);
41+
}
42+
43+
/**
44+
* @internal
45+
*/
46+
public readonly source: MappingType;
47+
48+
/**
49+
* @internal
50+
*/
51+
public readonly groups: MappingGroup[];
52+
53+
/**
54+
* @internal
55+
*/
56+
public readonly exclude: ExcludeList;
57+
58+
private constructor(source: MappingType, groups: MappingGroup[], exclude: ExcludeList) {
59+
this.source = source;
60+
this.groups = groups;
61+
this.exclude = exclude;
62+
}
63+
}
264

365
export interface RefactorOptions {
466
/**
@@ -16,30 +78,9 @@ export interface RefactorOptions {
1678
stacks?: StackSelector;
1779

1880
/**
19-
* A list of resources that will not be part of the refactor.
20-
* Elements of this list must be the _destination_ locations
21-
* that should be excluded, i.e., the location to which a
22-
* resource would be moved if the refactor were to happen.
23-
*
24-
* The format of the locations in the file can be either:
25-
*
26-
* - Stack name and logical ID (e.g. `Stack1.MyQueue`)
27-
* - A construct path (e.g. `Stack1/Foo/Bar/Resource`).
28-
*/
29-
exclude?: string[];
30-
31-
/**
32-
* An explicit mapping to be used by the toolkit (as opposed to letting the
33-
* toolkit itself compute the mapping).
34-
*/
35-
mappings?: MappingGroup[];
36-
37-
/**
38-
* Modifies the behavior of the 'mappings' option by swapping source and
39-
* destination locations. This is useful when you want to undo a refactor
40-
* that was previously applied.
81+
* How the toolkit should obtain the mappings
4182
*/
42-
revert?: boolean;
83+
mappingSource?: MappingSource;
4384
}
4485

4586
export interface MappingGroup {

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,23 @@ import type { ResourceLocation } from './cloudformation';
55

66
export interface ExcludeList {
77
isExcluded(location: ResourceLocation): boolean;
8+
9+
union(other: ExcludeList): ExcludeList;
10+
}
11+
12+
abstract class AbstractExcludeList implements ExcludeList {
13+
abstract isExcluded(location: ResourceLocation): boolean;
14+
15+
union(other: ExcludeList): ExcludeList {
16+
return new UnionExcludeList([this, other]);
17+
}
818
}
919

10-
export class ManifestExcludeList implements ExcludeList {
20+
export class ManifestExcludeList extends AbstractExcludeList {
1121
private readonly excludedLocations: CfnResourceLocation[];
1222

1323
constructor(manifest: AssemblyManifest) {
24+
super();
1425
this.excludedLocations = this.getExcludedLocations(manifest);
1526
}
1627

@@ -48,11 +59,12 @@ export class ManifestExcludeList implements ExcludeList {
4859
}
4960
}
5061

51-
export class InMemoryExcludeList implements ExcludeList {
62+
export class InMemoryExcludeList extends AbstractExcludeList {
5263
private readonly excludedLocations: CfnResourceLocation[];
5364
private readonly excludedPaths: string[];
5465

5566
constructor(items: string[]) {
67+
super();
5668
this.excludedLocations = [];
5769
this.excludedPaths = [];
5870

@@ -85,28 +97,24 @@ export class InMemoryExcludeList implements ExcludeList {
8597
}
8698
}
8799

88-
export class UnionExcludeList implements ExcludeList {
100+
export class UnionExcludeList extends AbstractExcludeList {
89101
constructor(private readonly excludeLists: ExcludeList[]) {
102+
super();
90103
}
91104

92105
isExcluded(location: ResourceLocation): boolean {
93106
return this.excludeLists.some((excludeList) => excludeList.isExcluded(location));
94107
}
95108
}
96109

97-
export class NeverExclude implements ExcludeList {
110+
export class NeverExclude extends AbstractExcludeList {
98111
isExcluded(_location: ResourceLocation): boolean {
99112
return false;
100113
}
101114
}
102115

103-
export class AlwaysExclude implements ExcludeList {
116+
export class AlwaysExclude extends AbstractExcludeList {
104117
isExcluded(_location: ResourceLocation): boolean {
105118
return true;
106119
}
107120
}
108-
109-
export function fromManifestAndExclusionList(manifest: AssemblyManifest, exclude?: string[]): ExcludeList {
110-
return new UnionExcludeList([new ManifestExcludeList(manifest), new InMemoryExcludeList(exclude ?? [])]);
111-
}
112-

packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { appendObject, prepareDiff } from '../actions/diff/private';
2929
import type { DriftOptions, DriftResult } from '../actions/drift';
3030
import { type ListOptions } from '../actions/list';
3131
import type { MappingGroup, RefactorOptions } from '../actions/refactor';
32+
import { MappingSource } from '../actions/refactor';
3233
import { type RollbackOptions } from '../actions/rollback';
3334
import { type SynthOptions } from '../actions/synth';
3435
import type { WatchOptions } from '../actions/watch';
@@ -59,10 +60,11 @@ import { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/logs-
5960
import { Mode, PluginHost } from '../api/plugin';
6061
import {
6162
formatAmbiguitySectionHeader,
62-
formatAmbiguousMappings, formatMappingsHeader,
63+
formatAmbiguousMappings,
64+
formatMappingsHeader,
6365
formatTypedMappings,
64-
fromManifestAndExclusionList,
6566
getDeployedStacks,
67+
ManifestExcludeList,
6668
usePrescribedMappings,
6769
} from '../api/refactoring';
6870
import type { CloudFormationStack, ResourceMapping } from '../api/refactoring/cloudformation';
@@ -1075,21 +1077,14 @@ export class Toolkit extends CloudAssemblySourceBuilder {
10751077
}
10761078

10771079
private async _refactor(assembly: StackAssembly, ioHelper: IoHelper, options: RefactorOptions = {}): Promise<void> {
1078-
if (options.mappings && options.exclude) {
1079-
throw new ToolkitError("Cannot use both 'exclude' and 'mappings'.");
1080-
}
1081-
1082-
if (options.revert && !options.mappings) {
1083-
throw new ToolkitError("The 'revert' options can only be used with the 'mappings' option.");
1084-
}
1085-
10861080
if (!options.dryRun) {
10871081
throw new ToolkitError('Refactor is not available yet. Too see the proposed changes, use the --dry-run flag.');
10881082
}
10891083

10901084
const sdkProvider = await this.sdkProvider('refactor');
10911085
const stacks = await assembly.selectStacksV2(ALL_STACKS);
1092-
const exclude = fromManifestAndExclusionList(assembly.cloudAssembly.manifest, options.exclude);
1086+
const mappingSource = options.mappingSource ?? MappingSource.auto();
1087+
const exclude = mappingSource.exclude.union(new ManifestExcludeList(assembly.cloudAssembly.manifest));
10931088
const filteredStacks = await assembly.selectStacksV2(options.stacks ?? ALL_STACKS);
10941089

10951090
const refactoringContexts: RefactoringContext[] = [];
@@ -1099,7 +1094,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
10991094
deployedStacks,
11001095
localStacks,
11011096
filteredStacks: filteredStacks.stackArtifacts,
1102-
mappings: await getUserProvidedMappings(),
1097+
mappings: await getUserProvidedMappings(environment),
11031098
}));
11041099
}
11051100

@@ -1133,7 +1128,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
11331128
const environments: Map<string, cxapi.Environment> = new Map();
11341129

11351130
for (const stack of stacks.stackArtifacts) {
1136-
const environment = stack.environment;
1131+
const environment = await sdkProvider.resolveEnvironment(stack.environment);
11371132
const key = hashObject(environment);
11381133
environments.set(key, environment);
11391134
if (stackGroups.has(key)) {
@@ -1156,21 +1151,14 @@ export class Toolkit extends CloudAssemblySourceBuilder {
11561151
return result;
11571152
}
11581153

1159-
async function getUserProvidedMappings(): Promise<ResourceMapping[] | undefined> {
1160-
if (options.revert) {
1161-
return usePrescribedMappings(revert(options.mappings ?? []), sdkProvider);
1162-
}
1163-
if (options.mappings != null) {
1164-
return usePrescribedMappings(options.mappings ?? [], sdkProvider);
1165-
}
1166-
return undefined;
1167-
}
1154+
async function getUserProvidedMappings(environment: cxapi.Environment): Promise<ResourceMapping[] | undefined> {
1155+
return mappingSource.source == 'explicit'
1156+
? usePrescribedMappings(mappingSource.groups.filter(matchesEnvironment), sdkProvider)
1157+
: undefined;
11681158

1169-
function revert(mappings: MappingGroup[]): MappingGroup[] {
1170-
return mappings.map(group => ({
1171-
...group,
1172-
resources: Object.fromEntries(Object.entries(group.resources).map(([src, dst]) => ([dst, src]))),
1173-
}));
1159+
function matchesEnvironment(g: MappingGroup): boolean {
1160+
return g.account === environment.account && g.region === environment.region;
1161+
}
11741162
}
11751163
}
11761164

packages/@aws-cdk/toolkit-lib/test/actions/refactor.test.ts

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GetTemplateCommand, ListStacksCommand } from '@aws-sdk/client-cloudformation';
2-
import { StackSelectionStrategy, Toolkit } from '../../lib';
2+
import { MappingSource, StackSelectionStrategy, Toolkit } from '../../lib';
33
import { SdkProvider } from '../../lib/api/aws-auth/private';
44
import { builderFixture, TestIoHost } from '../_helpers';
55
import { mockCloudFormationClient, MockSdk } from '../_helpers/mock-sdk';
@@ -350,15 +350,15 @@ test('uses the explicit mapping when provided, instead of computing it on-the-fl
350350
const cx = await builderFixture(toolkit, 'stack-with-bucket');
351351
await toolkit.refactor(cx, {
352352
dryRun: true,
353-
mappings: [
353+
mappingSource: MappingSource.explicit([
354354
{
355355
account: '123456789012',
356356
region: 'us-east-1',
357357
resources: {
358358
'Stack1.OldLogicalID': 'Stack1.NewLogicalID',
359359
},
360360
},
361-
],
361+
]),
362362
});
363363

364364
// THEN
@@ -418,16 +418,14 @@ test('uses the reverse of an explicit mapping when provided', async () => {
418418
const cx = await builderFixture(toolkit, 'stack-with-bucket');
419419
await toolkit.refactor(cx, {
420420
dryRun: true,
421-
// ... this is the mapping we used...
422-
mappings: [{
421+
// ... this is the mapping we used, and now we want to revert it
422+
mappingSource: MappingSource.reverse([{
423423
account: '123456789012',
424424
region: 'us-east-1',
425425
resources: {
426426
'Stack1.OldLogicalID': 'Stack1.NewLogicalID',
427427
},
428-
}],
429-
// ...and now we want to revert it
430-
revert: true,
428+
}]),
431429
});
432430

433431
// THEN
@@ -450,35 +448,6 @@ test('uses the reverse of an explicit mapping when provided', async () => {
450448
);
451449
});
452450

453-
test('exclude and mappings are mutually exclusive', async () => {
454-
// WHEN
455-
const cx = await builderFixture(toolkit, 'stack-with-bucket');
456-
await expect(
457-
toolkit.refactor(cx, {
458-
dryRun: true,
459-
exclude: ['Stack1/OldLogicalID'],
460-
mappings: [{
461-
account: '123456789012',
462-
region: 'us-east-1',
463-
resources: {
464-
'Stack1.OldLogicalID': 'Stack1.NewLogicalID',
465-
},
466-
}],
467-
}),
468-
).rejects.toThrow(/Cannot use both 'exclude' and 'mappings'/);
469-
});
470-
471-
test('revert can only be used with mappings', async () => {
472-
// WHEN
473-
const cx = await builderFixture(toolkit, 'stack-with-bucket');
474-
await expect(
475-
toolkit.refactor(cx, {
476-
dryRun: true,
477-
revert: true,
478-
}),
479-
).rejects.toThrow(/The 'revert' options can only be used with the 'mappings' option/);
480-
});
481-
482451
test('computes one set of mappings per environment', async () => {
483452
// GIVEN
484453
mockCloudFormationClient

packages/aws-cdk/lib/cli/cdk-toolkit.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import * as path from 'path';
22
import { format } from 'util';
33
import { RequireApproval } from '@aws-cdk/cloud-assembly-schema';
44
import * as cxapi from '@aws-cdk/cx-api';
5-
import { StackSelectionStrategy, ToolkitError, PermissionChangeType, Toolkit } from '@aws-cdk/toolkit-lib';
65
import type { DeploymentMethod, ToolkitAction, ToolkitOptions } from '@aws-cdk/toolkit-lib';
6+
import { StackSelectionStrategy, ToolkitError, PermissionChangeType, Toolkit, MappingSource } from '@aws-cdk/toolkit-lib';
77
import * as chalk from 'chalk';
88
import * as chokidar from 'chokidar';
99
import * as fs from 'fs-extra';
@@ -1231,9 +1231,7 @@ export class CdkToolkit {
12311231
patterns: options.selector.patterns,
12321232
strategy: options.selector.patterns.length > 0 ? StackSelectionStrategy.PATTERN_MATCH : StackSelectionStrategy.ALL_STACKS,
12331233
},
1234-
exclude: await readExcludeFile(options.excludeFile),
1235-
mappings: await readMappingFile(options.mappingFile),
1236-
revert: options.revert,
1234+
mappingSource: await mappingSource(),
12371235
});
12381236
} catch (e) {
12391237
error((e as Error).message);
@@ -1266,6 +1264,16 @@ export class CdkToolkit {
12661264
}
12671265
return undefined;
12681266
}
1267+
1268+
async function mappingSource(): Promise<MappingSource> {
1269+
if (options.mappingFile != null) {
1270+
return MappingSource.explicit(await readMappingFile(options.mappingFile));
1271+
}
1272+
if (options.revert) {
1273+
return MappingSource.reverse(await readMappingFile(options.mappingFile));
1274+
}
1275+
return MappingSource.auto((await readExcludeFile(options.excludeFile)) ?? []);
1276+
}
12691277
}
12701278

12711279
private async selectStacksForList(patterns: string[]) {

0 commit comments

Comments
 (0)