Skip to content

Commit db4029c

Browse files
authored
fix(cli): hotswap output is different for different resources (#250)
Aligns hotswap output to always follow the pattern: `<CfnResourceType>: '<physical name>'` For example: ``` AWS::Lambda::Function 'my-func' AWS::CodeBuild::Project 'my-project' ``` Previously we used "friendly" type names for some resources, but not all. Custom descriptions are kept only for more complicated resources, e.g.: ``` Contents of AWS::S3::Bucket 'my-bucket' AWS::Lambda::Alias 'my-alias' for AWS::Lambda::Function 'my-func' ``` Arguably this reduces output fidelity to hotswap users, however it increases consistency and maintainability. --- Also attaches more information to each affected resources, opening up potential future changes and structured data interfaces. This completes the cleanup on the `hotswappableChanges` side. Next up is the `nonHotswappableChanges`. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent be378de commit db4029c

File tree

9 files changed

+87
-35
lines changed

9 files changed

+87
-35
lines changed

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff';
22
import type * as cxapi from '@aws-cdk/cx-api';
3+
import type { ResourceMetadata } from '../../resource-metadata/resource-metadata';
34

45
/**
56
* A resource affected by a change
@@ -24,6 +25,13 @@ export interface AffectedResource {
2425
* A physical name is not always available, e.g. new resources will not have one until after the deployment
2526
*/
2627
readonly physicalName?: string;
28+
/**
29+
* Resource metadata attached to the logical id from the cloud assembly
30+
*
31+
* This is only present if the resource is present in the current Cloud Assembly,
32+
* i.e. resource deletions will not have metadata.
33+
*/
34+
readonly metadata?: ResourceMetadata;
2735
}
2836

2937
/**
@@ -56,6 +64,10 @@ export interface HotswappableChange {
5664
* The resource change that is causing the hotswap.
5765
*/
5866
readonly cause: ResourceChange;
67+
/**
68+
* A list of resources that are being hotswapped as part of the change
69+
*/
70+
readonly resources: AffectedResource[];
5971
}
6072

6173
/**
@@ -72,4 +84,3 @@ export interface HotswapDeployment {
7284
*/
7385
readonly mode: 'hotswap-only' | 'fall-back';
7486
}
75-

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

+14-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ 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';
6+
import type { AffectedResource, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads';
77
import type { IMessageSpan, IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
88
import { IO, SPAN } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
99
import type { SDK, SdkProvider } from '../aws-auth';
@@ -157,7 +157,7 @@ async function hotswapDeployment(
157157
});
158158

159159
const stackChanges = cfn_diff.fullDiff(currentTemplate.deployedRootTemplate, stack.template);
160-
const { hotswappableChanges, nonHotswappableChanges } = await classifyResourceChanges(
160+
const { hotswapOperations, nonHotswappableChanges } = await classifyResourceChanges(
161161
stackChanges,
162162
evaluateCfnTemplate,
163163
sdk,
@@ -166,6 +166,8 @@ async function hotswapDeployment(
166166

167167
await logNonHotswappableChanges(ioSpan, nonHotswappableChanges, hotswapMode);
168168

169+
const hotswappableChanges = hotswapOperations.map(o => o.change);
170+
169171
// preserve classic hotswap behavior
170172
if (hotswapMode === 'fall-back') {
171173
if (nonHotswappableChanges.length > 0) {
@@ -179,7 +181,7 @@ async function hotswapDeployment(
179181
}
180182

181183
// apply the short-circuitable changes
182-
await applyAllHotswappableChanges(sdk, ioSpan, hotswappableChanges);
184+
await applyAllHotswappableChanges(sdk, ioSpan, hotswapOperations);
183185

184186
return {
185187
stack,
@@ -225,7 +227,7 @@ async function classifyResourceChanges(
225227
sdk,
226228
hotswapPropertyOverrides,
227229
);
228-
hotswappableResources.push(...nestedHotswappableResources.hotswappableChanges);
230+
hotswappableResources.push(...nestedHotswappableResources.hotswapOperations);
229231
nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappableChanges);
230232

231233
continue;
@@ -275,7 +277,7 @@ async function classifyResourceChanges(
275277
}
276278

277279
return {
278-
hotswappableChanges: hotswappableResources,
280+
hotswapOperations: hotswappableResources,
279281
nonHotswappableChanges: nonHotswappableResources,
280282
};
281283
}
@@ -343,7 +345,7 @@ async function findNestedHotswappableChanges(
343345
const nestedStack = nestedStackTemplates[logicalId];
344346
if (!nestedStack.physicalName) {
345347
return {
346-
hotswappableChanges: [],
348+
hotswapOperations: [],
347349
nonHotswappableChanges: [
348350
{
349351
hotswappable: false,
@@ -470,8 +472,10 @@ async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan<any>, hots
470472
const customUserAgent = `cdk-hotswap/success-${hotswapOperation.service}`;
471473
sdk.appendCustomUserAgent(customUserAgent);
472474

473-
for (const name of hotswapOperation.resourceNames) {
474-
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(` ${ICON} %s`, chalk.bold(name))));
475+
const resourceText = (r: AffectedResource) => r.description ?? `${r.resourceType} '${r.physicalName ?? r.logicalId}'`;
476+
477+
for (const resource of hotswapOperation.change.resources) {
478+
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(` ${ICON} %s`, chalk.bold(resourceText(resource)))));
475479
}
476480

477481
// if the SDK call fails, an error will be thrown by the SDK
@@ -488,8 +492,8 @@ async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan<any>, hots
488492
throw e;
489493
}
490494

491-
for (const name of hotswapOperation.resourceNames) {
492-
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(`${ICON} %s %s`, chalk.bold(name), chalk.green('hotswapped!'))));
495+
for (const resource of hotswapOperation.change.resources) {
496+
await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(`${ICON} %s %s`, chalk.bold(resourceText(resource)), chalk.green('hotswapped!'))));
493497
}
494498

495499
sdk.removeCustomUserAgent(customUserAgent);

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,15 @@ export async function isHotswappableAppSyncChange(
6464
ret.push({
6565
change: {
6666
cause: change,
67+
resources: [{
68+
logicalId,
69+
resourceType: change.newValue.Type,
70+
physicalName,
71+
metadata: evaluateCfnTemplate.metadataFor(logicalId),
72+
}],
6773
},
6874
hotswappable: true,
6975
service: 'appsync',
70-
resourceNames: [`${change.newValue.Type} '${physicalName}'`],
7176
apply: async (sdk: SDK) => {
7277
const sdkProperties: { [name: string]: any } = {
7378
...change.oldValue.Properties,

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,15 @@ export async function isHotswappableCodeBuildProjectChange(
3939
ret.push({
4040
change: {
4141
cause: change,
42+
resources: [{
43+
logicalId: logicalId,
44+
resourceType: change.newValue.Type,
45+
physicalName: projectName,
46+
metadata: evaluateCfnTemplate.metadataFor(logicalId),
47+
}],
4248
},
4349
hotswappable: true,
4450
service: 'codebuild',
45-
resourceNames: [`CodeBuild Project '${projectName}'`],
4651
apply: async (sdk: SDK) => {
4752
updateProjectInput.name = projectName;
4853

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

+2-7
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface HotswapResult {
2323
/**
2424
* The changes that were deemed hotswappable
2525
*/
26-
readonly hotswappableChanges: any[];
26+
readonly hotswappableChanges: HotswappableChange[];
2727
/**
2828
* The changes that were deemed not hotswappable
2929
*/
@@ -47,11 +47,6 @@ export interface HotswapOperation {
4747
*/
4848
readonly change: HotswappableChange;
4949

50-
/**
51-
* The names of the resources being hotswapped.
52-
*/
53-
readonly resourceNames: string[];
54-
5550
/**
5651
* Applies the hotswap operation
5752
*/
@@ -80,7 +75,7 @@ export interface NonHotswappableChange {
8075
export type ChangeHotswapResult = Array<HotswapOperation | NonHotswappableChange>;
8176

8277
export interface ClassifiedResourceChanges {
83-
hotswappableChanges: HotswapOperation[];
78+
hotswapOperations: HotswapOperation[];
8479
nonHotswappableChanges: NonHotswappableChange[];
8580
}
8681

packages/aws-cdk/lib/api/hotswap/ecs-services.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ export async function isHotswappableEcsServiceChange(
4141
for (const ecsServiceResource of ecsServiceResourcesReferencingTaskDef) {
4242
const serviceArn = await evaluateCfnTemplate.findPhysicalNameFor(ecsServiceResource.LogicalId);
4343
if (serviceArn) {
44-
ecsServicesReferencingTaskDef.push({ serviceArn });
44+
ecsServicesReferencingTaskDef.push({
45+
logicalId: ecsServiceResource.LogicalId,
46+
serviceArn,
47+
});
4548
}
4649
}
4750
if (ecsServicesReferencingTaskDef.length === 0) {
@@ -69,13 +72,23 @@ export async function isHotswappableEcsServiceChange(
6972
ret.push({
7073
change: {
7174
cause: change,
75+
resources: [
76+
{
77+
logicalId,
78+
resourceType: change.newValue.Type,
79+
physicalName: await taskDefinitionResource.Family,
80+
metadata: evaluateCfnTemplate.metadataFor(logicalId),
81+
},
82+
...ecsServicesReferencingTaskDef.map((ecsService) => ({
83+
resourceType: ECS_SERVICE_RESOURCE_TYPE,
84+
physicalName: ecsService.serviceArn.split('/')[2],
85+
logicalId: ecsService.logicalId,
86+
metadata: evaluateCfnTemplate.metadataFor(ecsService.logicalId),
87+
})),
88+
],
7289
},
7390
hotswappable: true,
7491
service: 'ecs-service',
75-
resourceNames: [
76-
`ECS Task Definition '${await taskDefinitionResource.Family}'`,
77-
...ecsServicesReferencingTaskDef.map((ecsService) => `ECS Service '${ecsService.serviceArn.split('/')[2]}'`),
78-
],
7992
apply: async (sdk: SDK) => {
8093
// Step 1 - update the changed TaskDefinition, creating a new TaskDefinition Revision
8194
// we need to lowercase the evaluated TaskDef from CloudFormation,
@@ -141,6 +154,7 @@ export async function isHotswappableEcsServiceChange(
141154
}
142155

143156
interface EcsService {
157+
readonly logicalId: string;
144158
readonly serviceArn: string;
145159
}
146160

packages/aws-cdk/lib/api/hotswap/lambda-functions.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,18 @@ export async function isHotswappableLambdaFunctionChange(
6161
ret.push({
6262
change: {
6363
cause: change,
64+
resources: [
65+
{
66+
logicalId,
67+
resourceType: change.newValue.Type,
68+
physicalName: functionName,
69+
metadata: evaluateCfnTemplate.metadataFor(logicalId),
70+
},
71+
...dependencies,
72+
],
6473
},
6574
hotswappable: true,
6675
service: 'lambda',
67-
resourceNames: [
68-
`Lambda Function '${functionName}'`,
69-
...dependencies.map(d => d.description ?? `${d.resourceType} '${d.physicalName}'`),
70-
],
7176
apply: async (sdk: SDK) => {
7277
const lambda = sdk.lambda();
7378
const operations: Promise<any>[] = [];
@@ -363,17 +368,19 @@ async function dependantResources(
363368
const name = await evaluateCfnTemplate.evaluateCfnExpression(a.Properties?.Name);
364369
return {
365370
logicalId: a.LogicalId,
371+
resourceType: a.Type,
366372
physicalName: name,
367-
resourceType: 'AWS::Lambda::Alias',
368-
description: `Lambda Alias '${name}' for Function '${functionName}'`,
373+
description: `${a.Type} '${name}' for AWS::Lambda::Function '${functionName}'`,
374+
metadata: evaluateCfnTemplate.metadataFor(a.LogicalId),
369375
};
370376
}));
371377

372378
const versions = candidates.versionsReferencingFunction.map((v) => (
373379
{
374380
logicalId: v.LogicalId,
375381
resourceType: v.Type,
376-
description: `Lambda Version for Function '${functionName}'`,
382+
description: `${v.Type} for AWS::Lambda::Function '${functionName}'`,
383+
metadata: evaluateCfnTemplate.metadataFor(v.LogicalId),
377384
}
378385
));
379386

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn';
1212
const CDK_BUCKET_DEPLOYMENT_CFN_TYPE = 'Custom::CDKBucketDeployment';
1313

1414
export async function isHotswappableS3BucketDeploymentChange(
15-
_logicalId: string,
15+
logicalId: string,
1616
change: ResourceChange,
1717
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
1818
): Promise<ChangeHotswapResult> {
@@ -39,10 +39,16 @@ export async function isHotswappableS3BucketDeploymentChange(
3939
ret.push({
4040
change: {
4141
cause: change,
42+
resources: [{
43+
logicalId,
44+
physicalName: customResourceProperties.DestinationBucketName,
45+
resourceType: CDK_BUCKET_DEPLOYMENT_CFN_TYPE,
46+
description: `Contents of AWS::S3::Bucket '${customResourceProperties.DestinationBucketName}'`,
47+
metadata: evaluateCfnTemplate.metadataFor(logicalId),
48+
}],
4249
},
4350
hotswappable: true,
4451
service: 'custom-s3-deployment',
45-
resourceNames: [`Contents of S3 Bucket '${customResourceProperties.DestinationBucketName}'`],
4652
apply: async (sdk: SDK) => {
4753
await sdk.lambda().invokeCommand({
4854
FunctionName: functionName,

packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ export async function isHotswappableStateMachineChange(
3434
ret.push({
3535
change: {
3636
cause: change,
37+
resources: [{
38+
logicalId,
39+
resourceType: change.newValue.Type,
40+
physicalName: stateMachineArn?.split(':')[6],
41+
metadata: evaluateCfnTemplate.metadataFor(logicalId),
42+
}],
3743
},
3844
hotswappable: true,
3945
service: 'stepfunctions-service',
40-
resourceNames: [`${change.newValue.Type} '${stateMachineArn?.split(':')[6]}'`],
4146
apply: async (sdk: SDK) => {
4247
// not passing the optional properties leaves them unchanged
4348
await sdk.stepFunctions().updateStateMachine({

0 commit comments

Comments
 (0)