Skip to content

Commit 814c45d

Browse files
authored
refactor(cli): various code re-organization in hotswap code (#254)
Various straight-forward code re-organization changes and renames: - moved helper functions into `util` - `ChangeHotswapResult` -> `HotswapChange[]` - shared `ClassifiedResourceChanges` -> localized `ClassifiedChanges` - `NonHotswappableChange` -> `RejectedChange` - `rejectedChanges` -> `rejectedProperties` cause that's what they are - simplified and aligned `reportNonHotswappableChange` and `reportNonHotswappableResource` to be non-mutating and only take actually used args --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent a0de04e commit 814c45d

File tree

11 files changed

+135
-154
lines changed

11 files changed

+135
-154
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/util/objects.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,35 @@ export function splitBySize(data: any, maxSizeBytes: number): [any, any] {
215215
];
216216
}
217217
}
218+
219+
type Exclude = { [key: string]: Exclude | true };
220+
221+
/**
222+
* This function transforms all keys (recursively) in the provided `val` object.
223+
*
224+
* @param val The object whose keys need to be transformed.
225+
* @param transform The function that will be applied to each key.
226+
* @param exclude The keys that will not be transformed and copied to output directly
227+
* @returns A new object with the same values as `val`, but with all keys transformed according to `transform`.
228+
*/
229+
export function transformObjectKeys(val: any, transform: (str: string) => string, exclude: Exclude = {}): any {
230+
if (val == null || typeof val !== 'object') {
231+
return val;
232+
}
233+
if (Array.isArray(val)) {
234+
// For arrays we just pass parent's exclude object directly
235+
// since it makes no sense to specify different exclude options for each array element
236+
return val.map((input: any) => transformObjectKeys(input, transform, exclude));
237+
}
238+
const ret: { [k: string]: any } = {};
239+
for (const [k, v] of Object.entries(val)) {
240+
const childExclude = exclude[k];
241+
if (childExclude === true) {
242+
// we don't transform this object if the key is specified in exclude
243+
ret[transform(k)] = v;
244+
} else {
245+
ret[transform(k)] = transformObjectKeys(v, transform, childExclude);
246+
}
247+
}
248+
return ret;
249+
}

packages/@aws-cdk/tmp-toolkit-helpers/src/util/string-manipulation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,10 @@ function roundPercentage(num: number): number {
3535
function millisecondsToSeconds(num: number): number {
3636
return num / 1000;
3737
}
38+
39+
/**
40+
* This function lower cases the first character of the string provided.
41+
*/
42+
export function lowerCaseFirstCharacter(str: string): string {
43+
return str.length > 0 ? `${str[0].toLowerCase()}${str.slice(1)}` : str;
44+
}

packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@ import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-templ
1717
import { isHotswappableAppSyncChange } from '../hotswap/appsync-mapping-templates';
1818
import { isHotswappableCodeBuildProjectChange } from '../hotswap/code-build-projects';
1919
import type {
20-
ChangeHotswapResult,
20+
HotswapChange,
2121
HotswapOperation,
22-
NonHotswappableChange,
22+
RejectedChange,
2323
HotswapPropertyOverrides,
24-
ClassifiedResourceChanges,
2524
HotswapResult,
2625
} from '../hotswap/common';
2726
import {
2827
ICON,
29-
reportNonHotswappableChange,
30-
reportNonHotswappableResource,
28+
nonHotswappableResource,
3129
} from '../hotswap/common';
3230
import { isHotswappableEcsServiceChange } from '../hotswap/ecs-services';
3331
import { isHotswappableLambdaFunctionChange } from '../hotswap/lambda-functions';
@@ -48,7 +46,7 @@ type HotswapDetector = (
4846
change: ResourceChange,
4947
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
5048
hotswapPropertyOverrides: HotswapPropertyOverrides,
51-
) => Promise<ChangeHotswapResult>;
49+
) => Promise<HotswapChange[]>;
5250

5351
type HotswapMode = 'hotswap-only' | 'fall-back';
5452

@@ -72,17 +70,13 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = {
7270
logicalId: string,
7371
change: ResourceChange,
7472
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
75-
): Promise<ChangeHotswapResult> => {
73+
): Promise<HotswapChange[]> => {
7674
// If the policy is for a S3BucketDeploymentChange, we can ignore the change
7775
if (await skipChangeForS3DeployCustomResourcePolicy(logicalId, change, evaluateCfnTemplate)) {
7876
return [];
7977
}
8078

81-
return reportNonHotswappableResource(
82-
change,
83-
NonHotswappableReason.RESOURCE_UNSUPPORTED,
84-
'This resource type is not supported for hotswap deployments',
85-
);
79+
return [nonHotswappableResource(change)];
8680
},
8781

8882
'AWS::CDK::Metadata': async () => [],
@@ -162,7 +156,7 @@ async function hotswapDeployment(
162156
});
163157

164158
const stackChanges = cfn_diff.fullDiff(currentTemplate.deployedRootTemplate, stack.template);
165-
const { hotswapOperations, nonHotswappableChanges } = await classifyResourceChanges(
159+
const { hotswappable: hotswapOperations, nonHotswappable: nonHotswappableChanges } = await classifyResourceChanges(
166160
stackChanges,
167161
evaluateCfnTemplate,
168162
sdk,
@@ -196,6 +190,11 @@ async function hotswapDeployment(
196190
};
197191
}
198192

193+
interface ClassifiedChanges {
194+
hotswappable: HotswapOperation[];
195+
nonHotswappable: RejectedChange[];
196+
}
197+
199198
/**
200199
* Classifies all changes to all resources as either hotswappable or not.
201200
* Metadata changes are excluded from the list of (non)hotswappable resources.
@@ -206,19 +205,18 @@ async function classifyResourceChanges(
206205
sdk: SDK,
207206
nestedStackNames: { [nestedStackName: string]: NestedStackTemplates },
208207
hotswapPropertyOverrides: HotswapPropertyOverrides,
209-
): Promise<ClassifiedResourceChanges> {
208+
): Promise<ClassifiedChanges> {
210209
const resourceDifferences = getStackResourceDifferences(stackChanges);
211210

212-
const promises: Array<() => Promise<ChangeHotswapResult>> = [];
211+
const promises: Array<() => Promise<HotswapChange[]>> = [];
213212
const hotswappableResources = new Array<HotswapOperation>();
214-
const nonHotswappableResources = new Array<NonHotswappableChange>();
213+
const nonHotswappableResources = new Array<RejectedChange>();
215214
for (const logicalId of Object.keys(stackChanges.outputs.changes)) {
216215
nonHotswappableResources.push({
217216
hotswappable: false,
218217
reason: NonHotswappableReason.OUTPUT,
219218
description: 'output was changed',
220219
logicalId,
221-
rejectedChanges: [],
222220
resourceType: 'Stack Output',
223221
});
224222
}
@@ -233,8 +231,8 @@ async function classifyResourceChanges(
233231
sdk,
234232
hotswapPropertyOverrides,
235233
);
236-
hotswappableResources.push(...nestedHotswappableResources.hotswapOperations);
237-
nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappableChanges);
234+
hotswappableResources.push(...nestedHotswappableResources.hotswappable);
235+
nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappable);
238236

239237
continue;
240238
}
@@ -256,18 +254,12 @@ async function classifyResourceChanges(
256254
RESOURCE_DETECTORS[resourceType](logicalId, hotswappableChangeCandidate, evaluateCfnTemplate, hotswapPropertyOverrides),
257255
);
258256
} else {
259-
reportNonHotswappableChange(
260-
nonHotswappableResources,
261-
hotswappableChangeCandidate,
262-
NonHotswappableReason.RESOURCE_UNSUPPORTED,
263-
undefined,
264-
'This resource type is not supported for hotswap deployments',
265-
);
257+
nonHotswappableResources.push(nonHotswappableResource(hotswappableChangeCandidate));
266258
}
267259
}
268260

269261
// resolve all detector results
270-
const changesDetectionResults: Array<ChangeHotswapResult> = [];
262+
const changesDetectionResults: Array<HotswapChange[]> = [];
271263
for (const detectorResultPromises of promises) {
272264
// Constant set of promises per resource
273265
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
@@ -284,8 +276,8 @@ async function classifyResourceChanges(
284276
}
285277

286278
return {
287-
hotswapOperations: hotswappableResources,
288-
nonHotswappableChanges: nonHotswappableResources,
279+
hotswappable: hotswappableResources,
280+
nonHotswappable: nonHotswappableResources,
289281
};
290282
}
291283

@@ -348,18 +340,17 @@ async function findNestedHotswappableChanges(
348340
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
349341
sdk: SDK,
350342
hotswapPropertyOverrides: HotswapPropertyOverrides,
351-
): Promise<ClassifiedResourceChanges> {
343+
): Promise<ClassifiedChanges> {
352344
const nestedStack = nestedStackTemplates[logicalId];
353345
if (!nestedStack.physicalName) {
354346
return {
355-
hotswapOperations: [],
356-
nonHotswappableChanges: [
347+
hotswappable: [],
348+
nonHotswappable: [
357349
{
358350
hotswappable: false,
359351
logicalId,
360352
reason: NonHotswappableReason.NESTED_STACK_CREATION,
361353
description: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`,
362-
rejectedChanges: [],
363354
resourceType: 'AWS::CloudFormation::Stack',
364355
},
365356
],
@@ -425,14 +416,13 @@ function makeRenameDifference(
425416
function isCandidateForHotswapping(
426417
change: cfn_diff.ResourceDifference,
427418
logicalId: string,
428-
): HotswapOperation | NonHotswappableChange | ResourceChange {
419+
): RejectedChange | ResourceChange {
429420
// a resource has been removed OR a resource has been added; we can't short-circuit that change
430421
if (!change.oldValue) {
431422
return {
432423
hotswappable: false,
433424
resourceType: change.newValue!.Type,
434425
logicalId,
435-
rejectedChanges: [],
436426
reason: NonHotswappableReason.RESOURCE_CREATION,
437427
description: `resource '${logicalId}' was created by this deployment`,
438428
};
@@ -441,7 +431,6 @@ function isCandidateForHotswapping(
441431
hotswappable: false,
442432
resourceType: change.oldValue!.Type,
443433
logicalId,
444-
rejectedChanges: [],
445434
reason: NonHotswappableReason.RESOURCE_DELETION,
446435
description: `resource '${logicalId}' was destroyed by this deployment`,
447436
};
@@ -453,7 +442,6 @@ function isCandidateForHotswapping(
453442
hotswappable: false,
454443
resourceType: change.newValue?.Type,
455444
logicalId,
456-
rejectedChanges: [],
457445
reason: NonHotswappableReason.RESOURCE_TYPE_CHANGED,
458446
description: `resource '${logicalId}' had its type changed from '${change.oldValue?.Type}' to '${change.newValue?.Type}'`,
459447
};
@@ -530,7 +518,7 @@ function formatWaiterErrorResult(result: WaiterResult) {
530518

531519
async function logNonHotswappableChanges(
532520
ioSpan: IMessageSpan<any>,
533-
nonHotswappableChanges: NonHotswappableChange[],
521+
nonHotswappableChanges: RejectedChange[],
534522
hotswapMode: HotswapMode,
535523
): Promise<void> {
536524
if (nonHotswappableChanges.length === 0) {
@@ -560,12 +548,12 @@ async function logNonHotswappableChanges(
560548
}
561549

562550
for (const change of nonHotswappableChanges) {
563-
if (change.rejectedChanges.length > 0) {
551+
if (change.rejectedProperties?.length) {
564552
messages.push(format(
565553
' logicalID: %s, type: %s, rejected changes: %s, reason: %s',
566554
chalk.bold(change.logicalId),
567555
chalk.bold(change.resourceType),
568-
chalk.bold(change.rejectedChanges),
556+
chalk.bold(change.rejectedProperties),
569557
chalk.red(change.description),
570558
));
571559
} else {

packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import type {
33
GetSchemaCreationStatusCommandInput,
44
} from '@aws-sdk/client-appsync';
55
import {
6-
type ChangeHotswapResult,
6+
type HotswapChange,
77
classifyChanges,
8-
lowerCaseFirstCharacter,
9-
transformObjectKeys,
108
} from './common';
119
import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
1210
import { ToolkitError } from '../../toolkit/error';
11+
import { lowerCaseFirstCharacter, transformObjectKeys } from '../../util';
1312
import type { SDK } from '../aws-auth';
1413

1514
import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
@@ -18,7 +17,7 @@ export async function isHotswappableAppSyncChange(
1817
logicalId: string,
1918
change: ResourceChange,
2019
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
21-
): Promise<ChangeHotswapResult> {
20+
): Promise<HotswapChange[]> {
2221
const isResolver = change.newValue.Type === 'AWS::AppSync::Resolver';
2322
const isFunction = change.newValue.Type === 'AWS::AppSync::FunctionConfiguration';
2423
const isGraphQLSchema = change.newValue.Type === 'AWS::AppSync::GraphQLSchema';
@@ -27,7 +26,7 @@ export async function isHotswappableAppSyncChange(
2726
return [];
2827
}
2928

30-
const ret: ChangeHotswapResult = [];
29+
const ret: HotswapChange[] = [];
3130

3231
const classifiedChanges = classifyChanges(change, [
3332
'RequestMappingTemplate',

packages/aws-cdk/lib/api/hotswap/code-build-projects.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import type { UpdateProjectCommandInput } from '@aws-sdk/client-codebuild';
22
import {
3-
type ChangeHotswapResult,
3+
type HotswapChange,
44
classifyChanges,
5-
lowerCaseFirstCharacter,
6-
transformObjectKeys,
75
} from './common';
86
import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
7+
import { lowerCaseFirstCharacter, transformObjectKeys } from '../../util';
98
import type { SDK } from '../aws-auth';
109
import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
1110

1211
export async function isHotswappableCodeBuildProjectChange(
1312
logicalId: string,
1413
change: ResourceChange,
1514
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
16-
): Promise<ChangeHotswapResult> {
15+
): Promise<HotswapChange[]> {
1716
if (change.newValue.Type !== 'AWS::CodeBuild::Project') {
1817
return [];
1918
}
2019

21-
const ret: ChangeHotswapResult = [];
20+
const ret: HotswapChange[] = [];
2221

2322
const classifiedChanges = classifyChanges(change, ['Source', 'Environment', 'SourceVersion']);
2423
classifiedChanges.reportNonHotswappablePropertyChanges(ret);

0 commit comments

Comments
 (0)