From 8627b14b3b4093da968afbb9ebc8ba15485a1871 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Tue, 18 Mar 2025 16:40:13 +0000 Subject: [PATCH] fix(cli): hotswap output is different for different resources --- .../src/api/io/payloads/hotswap.ts | 13 +++++++++- .../api/deployments/hotswap-deployments.ts | 24 +++++++++++-------- .../api/hotswap/appsync-mapping-templates.ts | 7 +++++- .../lib/api/hotswap/code-build-projects.ts | 7 +++++- packages/aws-cdk/lib/api/hotswap/common.ts | 9 ++----- .../aws-cdk/lib/api/hotswap/ecs-services.ts | 24 +++++++++++++++---- .../lib/api/hotswap/lambda-functions.ts | 21 ++++++++++------ .../lib/api/hotswap/s3-bucket-deployments.ts | 10 ++++++-- .../hotswap/stepfunctions-state-machines.ts | 7 +++++- 9 files changed, 87 insertions(+), 35 deletions(-) diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap.ts index 462a84b18..d1dce5380 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap.ts @@ -1,5 +1,6 @@ import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff'; import type * as cxapi from '@aws-cdk/cx-api'; +import type { ResourceMetadata } from '../../resource-metadata/resource-metadata'; /** * A resource affected by a change @@ -24,6 +25,13 @@ export interface AffectedResource { * A physical name is not always available, e.g. new resources will not have one until after the deployment */ readonly physicalName?: string; + /** + * Resource metadata attached to the logical id from the cloud assembly + * + * This is only present if the resource is present in the current Cloud Assembly, + * i.e. resource deletions will not have metadata. + */ + readonly metadata?: ResourceMetadata; } /** @@ -56,6 +64,10 @@ export interface HotswappableChange { * The resource change that is causing the hotswap. */ readonly cause: ResourceChange; + /** + * A list of resources that are being hotswapped as part of the change + */ + readonly resources: AffectedResource[]; } /** @@ -72,4 +84,3 @@ export interface HotswapDeployment { */ readonly mode: 'hotswap-only' | 'fall-back'; } - diff --git a/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts b/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts index 4d476b5e9..cc4d7ed0e 100644 --- a/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts @@ -3,7 +3,7 @@ import * as cfn_diff from '@aws-cdk/cloudformation-diff'; import type * as cxapi from '@aws-cdk/cx-api'; import type { WaiterResult } from '@smithy/util-waiter'; import * as chalk from 'chalk'; -import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads'; +import type { AffectedResource, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads'; import type { IMessageSpan, IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; import { IO, SPAN } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; import type { SDK, SdkProvider } from '../aws-auth'; @@ -157,7 +157,7 @@ async function hotswapDeployment( }); const stackChanges = cfn_diff.fullDiff(currentTemplate.deployedRootTemplate, stack.template); - const { hotswappableChanges, nonHotswappableChanges } = await classifyResourceChanges( + const { hotswapOperations, nonHotswappableChanges } = await classifyResourceChanges( stackChanges, evaluateCfnTemplate, sdk, @@ -166,6 +166,8 @@ async function hotswapDeployment( await logNonHotswappableChanges(ioSpan, nonHotswappableChanges, hotswapMode); + const hotswappableChanges = hotswapOperations.map(o => o.change); + // preserve classic hotswap behavior if (hotswapMode === 'fall-back') { if (nonHotswappableChanges.length > 0) { @@ -179,7 +181,7 @@ async function hotswapDeployment( } // apply the short-circuitable changes - await applyAllHotswappableChanges(sdk, ioSpan, hotswappableChanges); + await applyAllHotswappableChanges(sdk, ioSpan, hotswapOperations); return { stack, @@ -225,7 +227,7 @@ async function classifyResourceChanges( sdk, hotswapPropertyOverrides, ); - hotswappableResources.push(...nestedHotswappableResources.hotswappableChanges); + hotswappableResources.push(...nestedHotswappableResources.hotswapOperations); nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappableChanges); continue; @@ -275,7 +277,7 @@ async function classifyResourceChanges( } return { - hotswappableChanges: hotswappableResources, + hotswapOperations: hotswappableResources, nonHotswappableChanges: nonHotswappableResources, }; } @@ -343,7 +345,7 @@ async function findNestedHotswappableChanges( const nestedStack = nestedStackTemplates[logicalId]; if (!nestedStack.physicalName) { return { - hotswappableChanges: [], + hotswapOperations: [], nonHotswappableChanges: [ { hotswappable: false, @@ -470,8 +472,10 @@ async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan, hots const customUserAgent = `cdk-hotswap/success-${hotswapOperation.service}`; sdk.appendCustomUserAgent(customUserAgent); - for (const name of hotswapOperation.resourceNames) { - await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(` ${ICON} %s`, chalk.bold(name)))); + const resourceText = (r: AffectedResource) => r.description ?? `${r.resourceType} '${r.physicalName ?? r.logicalId}'`; + + for (const resource of hotswapOperation.change.resources) { + await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(` ${ICON} %s`, chalk.bold(resourceText(resource))))); } // if the SDK call fails, an error will be thrown by the SDK @@ -488,8 +492,8 @@ async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan, hots throw e; } - for (const name of hotswapOperation.resourceNames) { - await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(`${ICON} %s %s`, chalk.bold(name), chalk.green('hotswapped!')))); + for (const resource of hotswapOperation.change.resources) { + await ioSpan.notify(IO.DEFAULT_TOOLKIT_INFO.msg(format(`${ICON} %s %s`, chalk.bold(resourceText(resource)), chalk.green('hotswapped!')))); } sdk.removeCustomUserAgent(customUserAgent); diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index 4c50da374..c2014c832 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -64,10 +64,15 @@ export async function isHotswappableAppSyncChange( ret.push({ change: { cause: change, + resources: [{ + logicalId, + resourceType: change.newValue.Type, + physicalName, + metadata: evaluateCfnTemplate.metadataFor(logicalId), + }], }, hotswappable: true, service: 'appsync', - resourceNames: [`${change.newValue.Type} '${physicalName}'`], apply: async (sdk: SDK) => { const sdkProperties: { [name: string]: any } = { ...change.oldValue.Properties, diff --git a/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts b/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts index d9efe8281..659a3e557 100644 --- a/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts +++ b/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts @@ -39,10 +39,15 @@ export async function isHotswappableCodeBuildProjectChange( ret.push({ change: { cause: change, + resources: [{ + logicalId: logicalId, + resourceType: change.newValue.Type, + physicalName: projectName, + metadata: evaluateCfnTemplate.metadataFor(logicalId), + }], }, hotswappable: true, service: 'codebuild', - resourceNames: [`CodeBuild Project '${projectName}'`], apply: async (sdk: SDK) => { updateProjectInput.name = projectName; diff --git a/packages/aws-cdk/lib/api/hotswap/common.ts b/packages/aws-cdk/lib/api/hotswap/common.ts index 99c0bf738..8be7cf6e8 100644 --- a/packages/aws-cdk/lib/api/hotswap/common.ts +++ b/packages/aws-cdk/lib/api/hotswap/common.ts @@ -23,7 +23,7 @@ export interface HotswapResult { /** * The changes that were deemed hotswappable */ - readonly hotswappableChanges: any[]; + readonly hotswappableChanges: HotswappableChange[]; /** * The changes that were deemed not hotswappable */ @@ -47,11 +47,6 @@ export interface HotswapOperation { */ readonly change: HotswappableChange; - /** - * The names of the resources being hotswapped. - */ - readonly resourceNames: string[]; - /** * Applies the hotswap operation */ @@ -80,7 +75,7 @@ export interface NonHotswappableChange { export type ChangeHotswapResult = Array; export interface ClassifiedResourceChanges { - hotswappableChanges: HotswapOperation[]; + hotswapOperations: HotswapOperation[]; nonHotswappableChanges: NonHotswappableChange[]; } diff --git a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts index e826b0c71..3e9d197a4 100644 --- a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts +++ b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts @@ -41,7 +41,10 @@ export async function isHotswappableEcsServiceChange( for (const ecsServiceResource of ecsServiceResourcesReferencingTaskDef) { const serviceArn = await evaluateCfnTemplate.findPhysicalNameFor(ecsServiceResource.LogicalId); if (serviceArn) { - ecsServicesReferencingTaskDef.push({ serviceArn }); + ecsServicesReferencingTaskDef.push({ + logicalId: ecsServiceResource.LogicalId, + serviceArn, + }); } } if (ecsServicesReferencingTaskDef.length === 0) { @@ -69,13 +72,23 @@ export async function isHotswappableEcsServiceChange( ret.push({ change: { cause: change, + resources: [ + { + logicalId, + resourceType: change.newValue.Type, + physicalName: await taskDefinitionResource.Family, + metadata: evaluateCfnTemplate.metadataFor(logicalId), + }, + ...ecsServicesReferencingTaskDef.map((ecsService) => ({ + resourceType: ECS_SERVICE_RESOURCE_TYPE, + physicalName: ecsService.serviceArn.split('/')[2], + logicalId: ecsService.logicalId, + metadata: evaluateCfnTemplate.metadataFor(ecsService.logicalId), + })), + ], }, hotswappable: true, service: 'ecs-service', - resourceNames: [ - `ECS Task Definition '${await taskDefinitionResource.Family}'`, - ...ecsServicesReferencingTaskDef.map((ecsService) => `ECS Service '${ecsService.serviceArn.split('/')[2]}'`), - ], apply: async (sdk: SDK) => { // Step 1 - update the changed TaskDefinition, creating a new TaskDefinition Revision // we need to lowercase the evaluated TaskDef from CloudFormation, @@ -141,6 +154,7 @@ export async function isHotswappableEcsServiceChange( } interface EcsService { + readonly logicalId: string; readonly serviceArn: string; } diff --git a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts index 14d6fd443..5adedf3d4 100644 --- a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts +++ b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts @@ -61,13 +61,18 @@ export async function isHotswappableLambdaFunctionChange( ret.push({ change: { cause: change, + resources: [ + { + logicalId, + resourceType: change.newValue.Type, + physicalName: functionName, + metadata: evaluateCfnTemplate.metadataFor(logicalId), + }, + ...dependencies, + ], }, hotswappable: true, service: 'lambda', - resourceNames: [ - `Lambda Function '${functionName}'`, - ...dependencies.map(d => d.description ?? `${d.resourceType} '${d.physicalName}'`), - ], apply: async (sdk: SDK) => { const lambda = sdk.lambda(); const operations: Promise[] = []; @@ -363,9 +368,10 @@ async function dependantResources( const name = await evaluateCfnTemplate.evaluateCfnExpression(a.Properties?.Name); return { logicalId: a.LogicalId, + resourceType: a.Type, physicalName: name, - resourceType: 'AWS::Lambda::Alias', - description: `Lambda Alias '${name}' for Function '${functionName}'`, + description: `${a.Type} '${name}' for AWS::Lambda::Function '${functionName}'`, + metadata: evaluateCfnTemplate.metadataFor(a.LogicalId), }; })); @@ -373,7 +379,8 @@ async function dependantResources( { logicalId: v.LogicalId, resourceType: v.Type, - description: `Lambda Version for Function '${functionName}'`, + description: `${v.Type} for AWS::Lambda::Function '${functionName}'`, + metadata: evaluateCfnTemplate.metadataFor(v.LogicalId), } )); diff --git a/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts b/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts index 2800337c1..0a293bbe1 100644 --- a/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts @@ -12,7 +12,7 @@ const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn'; const CDK_BUCKET_DEPLOYMENT_CFN_TYPE = 'Custom::CDKBucketDeployment'; export async function isHotswappableS3BucketDeploymentChange( - _logicalId: string, + logicalId: string, change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { @@ -39,10 +39,16 @@ export async function isHotswappableS3BucketDeploymentChange( ret.push({ change: { cause: change, + resources: [{ + logicalId, + physicalName: customResourceProperties.DestinationBucketName, + resourceType: CDK_BUCKET_DEPLOYMENT_CFN_TYPE, + description: `Contents of AWS::S3::Bucket '${customResourceProperties.DestinationBucketName}'`, + metadata: evaluateCfnTemplate.metadataFor(logicalId), + }], }, hotswappable: true, service: 'custom-s3-deployment', - resourceNames: [`Contents of S3 Bucket '${customResourceProperties.DestinationBucketName}'`], apply: async (sdk: SDK) => { await sdk.lambda().invokeCommand({ FunctionName: functionName, diff --git a/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts b/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts index 2344ae965..5fa253d62 100644 --- a/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts +++ b/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts @@ -34,10 +34,15 @@ export async function isHotswappableStateMachineChange( ret.push({ change: { cause: change, + resources: [{ + logicalId, + resourceType: change.newValue.Type, + physicalName: stateMachineArn?.split(':')[6], + metadata: evaluateCfnTemplate.metadataFor(logicalId), + }], }, hotswappable: true, service: 'stepfunctions-service', - resourceNames: [`${change.newValue.Type} '${stateMachineArn?.split(':')[6]}'`], apply: async (sdk: SDK) => { // not passing the optional properties leaves them unchanged await sdk.stepFunctions().updateStateMachine({