Skip to content

Commit 53ea256

Browse files
authored
fix(cli): hotswap is reporting and running changes that don't happen (#244)
This change is three fold: - We clean up the private `HotswapOperation` interface (née `HotswappableChange`) by removing two unused fields and adding in a new filed in preparation for structured data alongside hotswap messages - In the hotswap providers, pull gates outside of the apply function. This prevents hotswaps being reported when nothing would actually be done. These are local checks and now just run a little bit earlier. - In `lambda-functions.ts` don't report hotswappable changes for versions and aliases. These used to be reported with an empty array for `resourceNames` and a noop `apply` function. This means that literally nothing happens with these entries, since `apply` doesn't do anything and the CLI uses `resourceNames` to print anything. Just removing these as they are reported as part of a change to the function. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent a139c52 commit 53ea256

File tree

11 files changed

+194
-181
lines changed

11 files changed

+194
-181
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff';
2+
3+
/**
4+
* Represents a change in a resource
5+
*/
6+
export interface ResourceChange {
7+
/**
8+
* The logical ID of the resource which is being changed
9+
*/
10+
readonly logicalId: string;
11+
/**
12+
* The value the resource is being updated from
13+
*/
14+
readonly oldValue: Resource;
15+
/**
16+
* The value the resource is being updated to
17+
*/
18+
readonly newValue: Resource;
19+
/**
20+
* The changes made to the resource properties
21+
*/
22+
readonly propertyUpdates: Record<string, PropertyDifference<unknown>>;
23+
}
24+
25+
export interface HotswappableChange {
26+
/**
27+
* The resource change that is causing the hotswap.
28+
*/
29+
readonly cause: ResourceChange;
30+
}

packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './watch';
1313
export * from './stack-details';
1414
export * from './diff';
1515
export * from './logs-monitor';
16+
export * from './hotswap';

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

+13-11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as cfn_diff from '@aws-cdk/cloudformation-diff';
33
import type * as cxapi from '@aws-cdk/cx-api';
44
import type { WaiterResult } from '@smithy/util-waiter';
55
import * as chalk from 'chalk';
6+
import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads';
7+
import type { IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
68
import type { SDK, SdkProvider } from '../aws-auth';
79
import type { CloudFormationStack } from './cloudformation';
810
import type { NestedStackTemplates } from './nested-stack-helpers';
@@ -15,10 +17,10 @@ import { isHotswappableAppSyncChange } from '../hotswap/appsync-mapping-template
1517
import { isHotswappableCodeBuildProjectChange } from '../hotswap/code-build-projects';
1618
import type {
1719
ChangeHotswapResult,
18-
HotswappableChange,
20+
HotswapOperation,
1921
NonHotswappableChange,
20-
HotswappableChangeCandidate,
21-
HotswapPropertyOverrides, ClassifiedResourceChanges,
22+
HotswapPropertyOverrides,
23+
ClassifiedResourceChanges,
2224
} from '../hotswap/common';
2325
import {
2426
ICON,
@@ -35,15 +37,14 @@ import {
3537
import { isHotswappableStateMachineChange } from '../hotswap/stepfunctions-state-machines';
3638
import { Mode } from '../plugin';
3739
import type { SuccessfulDeployStackResult } from './deployment-result';
38-
import type { IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
3940

4041
// Must use a require() otherwise esbuild complains about calling a namespace
4142
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/consistent-type-imports
4243
const pLimit: typeof import('p-limit') = require('p-limit');
4344

4445
type HotswapDetector = (
4546
logicalId: string,
46-
change: HotswappableChangeCandidate,
47+
change: ResourceChange,
4748
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
4849
hotswapPropertyOverrides: HotswapPropertyOverrides,
4950
) => Promise<ChangeHotswapResult>;
@@ -66,7 +67,7 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = {
6667
'Custom::CDKBucketDeployment': isHotswappableS3BucketDeploymentChange,
6768
'AWS::IAM::Policy': async (
6869
logicalId: string,
69-
change: HotswappableChangeCandidate,
70+
change: ResourceChange,
7071
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
7172
): Promise<ChangeHotswapResult> => {
7273
// If the policy is for a S3BucketDeploymentChange, we can ignore the change
@@ -154,7 +155,7 @@ async function classifyResourceChanges(
154155
const resourceDifferences = getStackResourceDifferences(stackChanges);
155156

156157
const promises: Array<() => Promise<ChangeHotswapResult>> = [];
157-
const hotswappableResources = new Array<HotswappableChange>();
158+
const hotswappableResources = new Array<HotswapOperation>();
158159
const nonHotswappableResources = new Array<NonHotswappableChange>();
159160
for (const logicalId of Object.keys(stackChanges.outputs.changes)) {
160161
nonHotswappableResources.push({
@@ -323,7 +324,8 @@ async function findNestedHotswappableChanges(
323324
evaluateNestedCfnTemplate,
324325
sdk,
325326
nestedStackTemplates[logicalId].nestedStackTemplates,
326-
hotswapPropertyOverrides);
327+
hotswapPropertyOverrides,
328+
);
327329
}
328330

329331
/** Returns 'true' if a pair of changes is for the same resource. */
@@ -365,7 +367,7 @@ function makeRenameDifference(
365367
function isCandidateForHotswapping(
366368
change: cfn_diff.ResourceDifference,
367369
logicalId: string,
368-
): HotswappableChange | NonHotswappableChange | HotswappableChangeCandidate {
370+
): HotswapOperation | NonHotswappableChange | ResourceChange {
369371
// a resource has been removed OR a resource has been added; we can't short-circuit that change
370372
if (!change.oldValue) {
371373
return {
@@ -404,7 +406,7 @@ function isCandidateForHotswapping(
404406
};
405407
}
406408

407-
async function applyAllHotswappableChanges(sdk: SDK, ioHelper: IoHelper, hotswappableChanges: HotswappableChange[]): Promise<void[]> {
409+
async function applyAllHotswappableChanges(sdk: SDK, ioHelper: IoHelper, hotswappableChanges: HotswapOperation[]): Promise<void[]> {
408410
if (hotswappableChanges.length > 0) {
409411
await ioHelper.notify(info(`\n${ICON} hotswapping resources:`));
410412
}
@@ -415,7 +417,7 @@ async function applyAllHotswappableChanges(sdk: SDK, ioHelper: IoHelper, hotswap
415417
})));
416418
}
417419

418-
async function applyHotswappableChange(sdk: SDK, ioHelper: IoHelper, hotswapOperation: HotswappableChange): Promise<void> {
420+
async function applyHotswappableChange(sdk: SDK, ioHelper: IoHelper, hotswapOperation: HotswapOperation): Promise<void> {
419421
// note the type of service that was successfully hotswapped in the User-Agent
420422
const customUserAgent = `cdk-hotswap/success-${hotswapOperation.service}`;
421423
sdk.appendCustomUserAgent(customUserAgent);

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

+11-8
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ import type {
55
import {
66
type ChangeHotswapResult,
77
classifyChanges,
8-
type HotswappableChangeCandidate,
98
lowerCaseFirstCharacter,
109
transformObjectKeys,
1110
} from './common';
11+
import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
1212
import { ToolkitError } from '../../toolkit/error';
1313
import type { SDK } from '../aws-auth';
1414

1515
import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
1616

1717
export async function isHotswappableAppSyncChange(
1818
logicalId: string,
19-
change: HotswappableChangeCandidate,
19+
change: ResourceChange,
2020
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
2121
): Promise<ChangeHotswapResult> {
2222
const isResolver = change.newValue.Type === 'AWS::AppSync::Resolver';
@@ -55,17 +55,20 @@ export async function isHotswappableAppSyncChange(
5555
} else {
5656
physicalName = arn;
5757
}
58+
59+
// nothing do here
60+
if (!physicalName) {
61+
return ret;
62+
}
63+
5864
ret.push({
65+
change: {
66+
cause: change,
67+
},
5968
hotswappable: true,
60-
resourceType: change.newValue.Type,
61-
propsChanged: namesOfHotswappableChanges,
6269
service: 'appsync',
6370
resourceNames: [`${change.newValue.Type} '${physicalName}'`],
6471
apply: async (sdk: SDK) => {
65-
if (!physicalName) {
66-
return;
67-
}
68-
6972
const sdkProperties: { [name: string]: any } = {
7073
...change.oldValue.Properties,
7174
Definition: change.newValue.Properties?.Definition,

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

+11-7
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import type { UpdateProjectCommandInput } from '@aws-sdk/client-codebuild';
22
import {
33
type ChangeHotswapResult,
44
classifyChanges,
5-
type HotswappableChangeCandidate,
65
lowerCaseFirstCharacter,
76
transformObjectKeys,
87
} from './common';
8+
import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
99
import type { SDK } from '../aws-auth';
1010
import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
1111

1212
export async function isHotswappableCodeBuildProjectChange(
1313
logicalId: string,
14-
change: HotswappableChangeCandidate,
14+
change: ResourceChange,
1515
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
1616
): Promise<ChangeHotswapResult> {
1717
if (change.newValue.Type !== 'AWS::CodeBuild::Project') {
@@ -30,16 +30,20 @@ export async function isHotswappableCodeBuildProjectChange(
3030
logicalId,
3131
change.newValue.Properties?.Name,
3232
);
33+
34+
// nothing to do jere
35+
if (!projectName) {
36+
return ret;
37+
}
38+
3339
ret.push({
40+
change: {
41+
cause: change,
42+
},
3443
hotswappable: true,
35-
resourceType: change.newValue.Type,
36-
propsChanged: classifiedChanges.namesOfHotswappableProps,
3744
service: 'codebuild',
3845
resourceNames: [`CodeBuild Project '${projectName}'`],
3946
apply: async (sdk: SDK) => {
40-
if (!projectName) {
41-
return;
42-
}
4347
updateProjectInput.name = projectName;
4448

4549
for (const updatedPropName in change.propertyUpdates) {

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

+22-43
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
1-
import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff';
1+
import type { PropertyDifference } from '@aws-cdk/cloudformation-diff';
2+
import type { HotswappableChange, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap';
23
import { ToolkitError } from '../../toolkit/error';
34
import type { SDK } from '../aws-auth';
45

56
export const ICON = '✨';
67

7-
export interface HotswappableChange {
8+
export interface HotswapOperation {
9+
/**
10+
* Marks the operation as hotswappable
11+
*/
812
readonly hotswappable: true;
9-
readonly resourceType: string;
10-
readonly propsChanged: Array<string>;
13+
1114
/**
1215
* The name of the service being hotswapped.
1316
* Used to set a custom User-Agent for SDK calls.
1417
*/
1518
readonly service: string;
1619

20+
/**
21+
* Description of the change that is applied as part of the operation
22+
*/
23+
readonly change: HotswappableChange;
24+
1725
/**
1826
* The names of the resources being hotswapped.
1927
*/
2028
readonly resourceNames: string[];
2129

30+
/**
31+
* Applies the hotswap operation
32+
*/
2233
readonly apply: (sdk: SDK) => Promise<void>;
2334
}
2435

@@ -41,10 +52,10 @@ export interface NonHotswappableChange {
4152
readonly hotswapOnlyVisible?: boolean;
4253
}
4354

44-
export type ChangeHotswapResult = Array<HotswappableChange | NonHotswappableChange>;
55+
export type ChangeHotswapResult = Array<HotswapOperation | NonHotswappableChange>;
4556

4657
export interface ClassifiedResourceChanges {
47-
hotswappableChanges: HotswappableChange[];
58+
hotswappableChanges: HotswapOperation[];
4859
nonHotswappableChanges: NonHotswappableChange[];
4960
}
5061

@@ -65,38 +76,6 @@ export enum HotswapMode {
6576
FULL_DEPLOYMENT = 'full-deployment',
6677
}
6778

68-
/**
69-
* Represents a change that can be hotswapped.
70-
*/
71-
export class HotswappableChangeCandidate {
72-
/**
73-
* The logical ID of the resource which is being changed
74-
*/
75-
public readonly logicalId: string;
76-
77-
/**
78-
* The value the resource is being updated from
79-
*/
80-
public readonly oldValue: Resource;
81-
82-
/**
83-
* The value the resource is being updated to
84-
*/
85-
public readonly newValue: Resource;
86-
87-
/**
88-
* The changes made to the resource properties
89-
*/
90-
public readonly propertyUpdates: PropDiffs;
91-
92-
public constructor(logicalId: string, oldValue: Resource, newValue: Resource, propertyUpdates: PropDiffs) {
93-
this.logicalId = logicalId;
94-
this.oldValue = oldValue;
95-
this.newValue = newValue;
96-
this.propertyUpdates = propertyUpdates;
97-
}
98-
}
99-
10079
type Exclude = { [key: string]: Exclude | true };
10180

10281
/**
@@ -182,11 +161,11 @@ export function lowerCaseFirstCharacter(str: string): string {
182161
return str.length > 0 ? `${str[0].toLowerCase()}${str.slice(1)}` : str;
183162
}
184163

185-
export type PropDiffs = Record<string, PropertyDifference<any>>;
164+
type PropDiffs = Record<string, PropertyDifference<any>>;
186165

187166
export class ClassifiedChanges {
188167
public constructor(
189-
public readonly change: HotswappableChangeCandidate,
168+
public readonly change: ResourceChange,
190169
public readonly hotswappableProps: PropDiffs,
191170
public readonly nonHotswappableProps: PropDiffs,
192171
) {
@@ -212,7 +191,7 @@ export class ClassifiedChanges {
212191
}
213192
}
214193

215-
export function classifyChanges(xs: HotswappableChangeCandidate, hotswappablePropNames: string[]): ClassifiedChanges {
194+
export function classifyChanges(xs: ResourceChange, hotswappablePropNames: string[]): ClassifiedChanges {
216195
const hotswappableProps: PropDiffs = {};
217196
const nonHotswappableProps: PropDiffs = {};
218197

@@ -229,7 +208,7 @@ export function classifyChanges(xs: HotswappableChangeCandidate, hotswappablePro
229208

230209
export function reportNonHotswappableChange(
231210
ret: ChangeHotswapResult,
232-
change: HotswappableChangeCandidate,
211+
change: ResourceChange,
233212
nonHotswappableProps?: PropDiffs,
234213
reason?: string,
235214
hotswapOnlyVisible?: boolean,
@@ -249,7 +228,7 @@ export function reportNonHotswappableChange(
249228
}
250229

251230
export function reportNonHotswappableResource(
252-
change: HotswappableChangeCandidate,
231+
change: ResourceChange,
253232
reason?: string,
254233
): ChangeHotswapResult {
255234
return [

0 commit comments

Comments
 (0)