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 new file mode 100644 index 000000000..b37394e75 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap.ts @@ -0,0 +1,30 @@ +import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff'; + +/** + * Represents a change in a resource + */ +export interface ResourceChange { + /** + * The logical ID of the resource which is being changed + */ + readonly logicalId: string; + /** + * The value the resource is being updated from + */ + readonly oldValue: Resource; + /** + * The value the resource is being updated to + */ + readonly newValue: Resource; + /** + * The changes made to the resource properties + */ + readonly propertyUpdates: Record>; +} + +export interface HotswappableChange { + /** + * The resource change that is causing the hotswap. + */ + readonly cause: ResourceChange; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts index 883a8cc5f..e6b73b48a 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/index.ts @@ -13,3 +13,4 @@ export * from './watch'; export * from './stack-details'; export * from './diff'; export * from './logs-monitor'; +export * from './hotswap'; diff --git a/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts b/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts index e90b63fa1..20474e21e 100644 --- a/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts @@ -3,6 +3,8 @@ 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 { IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; import type { SDK, SdkProvider } from '../aws-auth'; import type { CloudFormationStack } from './cloudformation'; import type { NestedStackTemplates } from './nested-stack-helpers'; @@ -15,10 +17,10 @@ import { isHotswappableAppSyncChange } from '../hotswap/appsync-mapping-template import { isHotswappableCodeBuildProjectChange } from '../hotswap/code-build-projects'; import type { ChangeHotswapResult, - HotswappableChange, + HotswapOperation, NonHotswappableChange, - HotswappableChangeCandidate, - HotswapPropertyOverrides, ClassifiedResourceChanges, + HotswapPropertyOverrides, + ClassifiedResourceChanges, } from '../hotswap/common'; import { ICON, @@ -35,7 +37,6 @@ import { import { isHotswappableStateMachineChange } from '../hotswap/stepfunctions-state-machines'; import { Mode } from '../plugin'; import type { SuccessfulDeployStackResult } from './deployment-result'; -import type { IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; // Must use a require() otherwise esbuild complains about calling a namespace // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/consistent-type-imports @@ -43,7 +44,7 @@ const pLimit: typeof import('p-limit') = require('p-limit'); type HotswapDetector = ( logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, hotswapPropertyOverrides: HotswapPropertyOverrides, ) => Promise; @@ -66,7 +67,7 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = { 'Custom::CDKBucketDeployment': isHotswappableS3BucketDeploymentChange, 'AWS::IAM::Policy': async ( logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise => { // If the policy is for a S3BucketDeploymentChange, we can ignore the change @@ -155,7 +156,7 @@ async function classifyResourceChanges( const resourceDifferences = getStackResourceDifferences(stackChanges); const promises: Array<() => Promise> = []; - const hotswappableResources = new Array(); + const hotswappableResources = new Array(); const nonHotswappableResources = new Array(); for (const logicalId of Object.keys(stackChanges.outputs.changes)) { nonHotswappableResources.push({ @@ -324,7 +325,8 @@ async function findNestedHotswappableChanges( evaluateNestedCfnTemplate, sdk, nestedStackTemplates[logicalId].nestedStackTemplates, - hotswapPropertyOverrides); + hotswapPropertyOverrides, + ); } /** Returns 'true' if a pair of changes is for the same resource. */ @@ -366,7 +368,7 @@ function makeRenameDifference( function isCandidateForHotswapping( change: cfn_diff.ResourceDifference, logicalId: string, -): HotswappableChange | NonHotswappableChange | HotswappableChangeCandidate { +): HotswapOperation | NonHotswappableChange | ResourceChange { // a resource has been removed OR a resource has been added; we can't short-circuit that change if (!change.oldValue) { return { @@ -405,7 +407,7 @@ function isCandidateForHotswapping( }; } -async function applyAllHotswappableChanges(sdk: SDK, ioHelper: IoHelper, hotswappableChanges: HotswappableChange[]): Promise { +async function applyAllHotswappableChanges(sdk: SDK, ioHelper: IoHelper, hotswappableChanges: HotswapOperation[]): Promise { if (hotswappableChanges.length > 0) { await ioHelper.notify(info(`\n${ICON} hotswapping resources:`)); } @@ -416,7 +418,7 @@ async function applyAllHotswappableChanges(sdk: SDK, ioHelper: IoHelper, hotswap }))); } -async function applyHotswappableChange(sdk: SDK, ioHelper: IoHelper, hotswapOperation: HotswappableChange): Promise { +async function applyHotswappableChange(sdk: SDK, ioHelper: IoHelper, hotswapOperation: HotswapOperation): Promise { // note the type of service that was successfully hotswapped in the User-Agent const customUserAgent = `cdk-hotswap/success-${hotswapOperation.service}`; sdk.appendCustomUserAgent(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 b6be17cbf..4c50da374 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -5,10 +5,10 @@ import type { import { type ChangeHotswapResult, classifyChanges, - type HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys, } from './common'; +import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import { ToolkitError } from '../../toolkit/error'; import type { SDK } from '../aws-auth'; @@ -16,7 +16,7 @@ import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation- export async function isHotswappableAppSyncChange( logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { const isResolver = change.newValue.Type === 'AWS::AppSync::Resolver'; @@ -55,17 +55,20 @@ export async function isHotswappableAppSyncChange( } else { physicalName = arn; } + + // nothing do here + if (!physicalName) { + return ret; + } + ret.push({ + change: { + cause: change, + }, hotswappable: true, - resourceType: change.newValue.Type, - propsChanged: namesOfHotswappableChanges, service: 'appsync', resourceNames: [`${change.newValue.Type} '${physicalName}'`], apply: async (sdk: SDK) => { - if (!physicalName) { - return; - } - const sdkProperties: { [name: string]: any } = { ...change.oldValue.Properties, Definition: change.newValue.Properties?.Definition, 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 0b2e57e6f..d9efe8281 100644 --- a/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts +++ b/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts @@ -2,16 +2,16 @@ import type { UpdateProjectCommandInput } from '@aws-sdk/client-codebuild'; import { type ChangeHotswapResult, classifyChanges, - type HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys, } from './common'; +import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import type { SDK } from '../aws-auth'; import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableCodeBuildProjectChange( logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { if (change.newValue.Type !== 'AWS::CodeBuild::Project') { @@ -30,16 +30,20 @@ export async function isHotswappableCodeBuildProjectChange( logicalId, change.newValue.Properties?.Name, ); + + // nothing to do jere + if (!projectName) { + return ret; + } + ret.push({ + change: { + cause: change, + }, hotswappable: true, - resourceType: change.newValue.Type, - propsChanged: classifiedChanges.namesOfHotswappableProps, service: 'codebuild', resourceNames: [`CodeBuild Project '${projectName}'`], apply: async (sdk: SDK) => { - if (!projectName) { - return; - } updateProjectInput.name = projectName; for (const updatedPropName in change.propertyUpdates) { diff --git a/packages/aws-cdk/lib/api/hotswap/common.ts b/packages/aws-cdk/lib/api/hotswap/common.ts index 6b23fc3da..0cf6a5c29 100644 --- a/packages/aws-cdk/lib/api/hotswap/common.ts +++ b/packages/aws-cdk/lib/api/hotswap/common.ts @@ -1,24 +1,35 @@ -import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff'; +import type { PropertyDifference } from '@aws-cdk/cloudformation-diff'; +import type { HotswappableChange, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import { ToolkitError } from '../../toolkit/error'; import type { SDK } from '../aws-auth'; export const ICON = '✨'; -export interface HotswappableChange { +export interface HotswapOperation { + /** + * Marks the operation as hotswappable + */ readonly hotswappable: true; - readonly resourceType: string; - readonly propsChanged: Array; + /** * The name of the service being hotswapped. * Used to set a custom User-Agent for SDK calls. */ readonly service: string; + /** + * Description of the change that is applied as part of the operation + */ + readonly change: HotswappableChange; + /** * The names of the resources being hotswapped. */ readonly resourceNames: string[]; + /** + * Applies the hotswap operation + */ readonly apply: (sdk: SDK) => Promise; } @@ -41,10 +52,10 @@ export interface NonHotswappableChange { readonly hotswapOnlyVisible?: boolean; } -export type ChangeHotswapResult = Array; +export type ChangeHotswapResult = Array; export interface ClassifiedResourceChanges { - hotswappableChanges: HotswappableChange[]; + hotswappableChanges: HotswapOperation[]; nonHotswappableChanges: NonHotswappableChange[]; } @@ -65,38 +76,6 @@ export enum HotswapMode { FULL_DEPLOYMENT = 'full-deployment', } -/** - * Represents a change that can be hotswapped. - */ -export class HotswappableChangeCandidate { - /** - * The logical ID of the resource which is being changed - */ - public readonly logicalId: string; - - /** - * The value the resource is being updated from - */ - public readonly oldValue: Resource; - - /** - * The value the resource is being updated to - */ - public readonly newValue: Resource; - - /** - * The changes made to the resource properties - */ - public readonly propertyUpdates: PropDiffs; - - public constructor(logicalId: string, oldValue: Resource, newValue: Resource, propertyUpdates: PropDiffs) { - this.logicalId = logicalId; - this.oldValue = oldValue; - this.newValue = newValue; - this.propertyUpdates = propertyUpdates; - } -} - type Exclude = { [key: string]: Exclude | true }; /** @@ -182,11 +161,11 @@ export function lowerCaseFirstCharacter(str: string): string { return str.length > 0 ? `${str[0].toLowerCase()}${str.slice(1)}` : str; } -export type PropDiffs = Record>; +type PropDiffs = Record>; export class ClassifiedChanges { public constructor( - public readonly change: HotswappableChangeCandidate, + public readonly change: ResourceChange, public readonly hotswappableProps: PropDiffs, public readonly nonHotswappableProps: PropDiffs, ) { @@ -212,7 +191,7 @@ export class ClassifiedChanges { } } -export function classifyChanges(xs: HotswappableChangeCandidate, hotswappablePropNames: string[]): ClassifiedChanges { +export function classifyChanges(xs: ResourceChange, hotswappablePropNames: string[]): ClassifiedChanges { const hotswappableProps: PropDiffs = {}; const nonHotswappableProps: PropDiffs = {}; @@ -229,7 +208,7 @@ export function classifyChanges(xs: HotswappableChangeCandidate, hotswappablePro export function reportNonHotswappableChange( ret: ChangeHotswapResult, - change: HotswappableChangeCandidate, + change: ResourceChange, nonHotswappableProps?: PropDiffs, reason?: string, hotswapOnlyVisible?: boolean, @@ -249,7 +228,7 @@ export function reportNonHotswappableChange( } export function reportNonHotswappableResource( - change: HotswappableChangeCandidate, + change: ResourceChange, reason?: string, ): ChangeHotswapResult { return [ diff --git a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts index 906fdb1db..e826b0c71 100644 --- a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts +++ b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts @@ -1,19 +1,21 @@ import type { HotswapPropertyOverrides, ChangeHotswapResult, - HotswappableChangeCandidate, } from './common'; import { classifyChanges, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys, } from './common'; +import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import type { SDK } from '../aws-auth'; import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; +const ECS_SERVICE_RESOURCE_TYPE = 'AWS::ECS::Service'; + export async function isHotswappableEcsServiceChange( logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, hotswapPropertyOverrides: HotswapPropertyOverrides, ): Promise { @@ -33,7 +35,7 @@ export async function isHotswappableEcsServiceChange( // find all ECS Services that reference the TaskDefinition that changed const resourcesReferencingTaskDef = evaluateCfnTemplate.findReferencesTo(logicalId); const ecsServiceResourcesReferencingTaskDef = resourcesReferencingTaskDef.filter( - (r) => r.Type === 'AWS::ECS::Service', + (r) => r.Type === ECS_SERVICE_RESOURCE_TYPE, ); const ecsServicesReferencingTaskDef = new Array(); for (const ecsServiceResource of ecsServiceResourcesReferencingTaskDef) { @@ -50,7 +52,7 @@ export async function isHotswappableEcsServiceChange( if (resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) { // if something besides an ECS Service is referencing the TaskDefinition, // hotswap is not possible in FALL_BACK mode - const nonEcsServiceTaskDefRefs = resourcesReferencingTaskDef.filter((r) => r.Type !== 'AWS::ECS::Service'); + const nonEcsServiceTaskDefRefs = resourcesReferencingTaskDef.filter((r) => r.Type !== ECS_SERVICE_RESOURCE_TYPE); for (const taskRef of nonEcsServiceTaskDefRefs) { reportNonHotswappableChange( ret, @@ -65,9 +67,10 @@ export async function isHotswappableEcsServiceChange( if (namesOfHotswappableChanges.length > 0) { const taskDefinitionResource = await prepareTaskDefinitionChange(evaluateCfnTemplate, logicalId, change); ret.push({ + change: { + cause: change, + }, hotswappable: true, - resourceType: change.newValue.Type, - propsChanged: namesOfHotswappableChanges, service: 'ecs-service', resourceNames: [ `ECS Task Definition '${await taskDefinitionResource.Family}'`, @@ -144,7 +147,7 @@ interface EcsService { async function prepareTaskDefinitionChange( evaluateCfnTemplate: EvaluateCloudFormationTemplate, logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, ) { const taskDefinitionResource: { [name: string]: any } = { ...change.oldValue.Properties, diff --git a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts index dbff616e7..2887fd263 100644 --- a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts +++ b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts @@ -1,7 +1,9 @@ import { Writable } from 'stream'; +import type { PropertyDifference } from '@aws-cdk/cloudformation-diff'; import type { FunctionConfiguration, UpdateFunctionConfigurationCommandInput } from '@aws-sdk/client-lambda'; -import type { PropDiffs, ChangeHotswapResult, HotswappableChangeCandidate } from './common'; +import type { ChangeHotswapResult } from './common'; import { classifyChanges } from './common'; +import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import { ToolkitError } from '../../toolkit/error'; import { flatMap } from '../../util'; import type { ILambdaClient, SDK } from '../aws-auth'; @@ -13,28 +15,18 @@ const archiver = require('archiver'); export async function isHotswappableLambdaFunctionChange( logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { - // if the change is for a Lambda Version, - // ignore it by returning an empty hotswap operation - - // we will publish a new version when we get to hotswapping the actual Function this Version points to, below + // if the change is for a Lambda Version, we just ignore it + // we will publish a new version when we get to hotswapping the actual Function this Version points to // (Versions can't be changed in CloudFormation anyway, they're immutable) if (change.newValue.Type === 'AWS::Lambda::Version') { - return [ - { - hotswappable: true, - resourceType: 'AWS::Lambda::Version', - resourceNames: [], - propsChanged: [], - service: 'lambda', - apply: async (_sdk: SDK) => { - }, - }, - ]; + return []; } // we handle Aliases specially too + // the actual alias update will happen if we change the function if (change.newValue.Type === 'AWS::Lambda::Alias') { return classifyAliasChanges(change); } @@ -52,38 +44,31 @@ export async function isHotswappableLambdaFunctionChange( change.newValue.Properties?.FunctionName, ); const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); - if (namesOfHotswappableChanges.length > 0) { + if (functionName && namesOfHotswappableChanges.length > 0) { + const lambdaCodeChange = await evaluateLambdaFunctionProps( + classifiedChanges.hotswappableProps, + change.newValue.Properties?.Runtime, + evaluateCfnTemplate, + ); + + // nothing to do here + if (lambdaCodeChange === undefined) { + return ret; + } + + const dependencies = await dependantResources(logicalId, functionName, evaluateCfnTemplate); + ret.push({ + change: { + cause: change, + }, hotswappable: true, - resourceType: change.newValue.Type, - propsChanged: namesOfHotswappableChanges, service: 'lambda', resourceNames: [ `Lambda Function '${functionName}'`, - // add Version here if we're publishing a new one - ...(await renderVersions(logicalId, evaluateCfnTemplate, [`Lambda Version for Function '${functionName}'`])), - // add any Aliases that we are hotswapping here - ...(await renderAliases( - logicalId, - evaluateCfnTemplate, - async (alias) => `Lambda Alias '${alias}' for Function '${functionName}'`, - )), + ...dependencies.map(d => d.description), ], apply: async (sdk: SDK) => { - const lambdaCodeChange = await evaluateLambdaFunctionProps( - classifiedChanges.hotswappableProps, - change.newValue.Properties?.Runtime, - evaluateCfnTemplate, - ); - if (lambdaCodeChange === undefined) { - return; - } - - if (!functionName) { - return; - } - - const { versionsReferencingFunction, aliasesNames } = await versionsAndAliases(logicalId, evaluateCfnTemplate); const lambda = sdk.lambda(); const operations: Promise[] = []; @@ -116,19 +101,21 @@ export async function isHotswappableLambdaFunctionChange( } // only if the code changed is there any point in publishing a new Version - if (versionsReferencingFunction.length > 0) { + const versions = dependencies.filter((d) => d.resourceType === 'AWS::Lambda::Version'); + if (versions.length) { const publishVersionPromise = lambda.publishVersion({ FunctionName: functionName, }); - if (aliasesNames.length > 0) { + const aliases = dependencies.filter((d) => d.resourceType === 'AWS::Lambda::Alias'); + if (aliases.length) { // we need to wait for the Version to finish publishing const versionUpdate = await publishVersionPromise; - for (const alias of aliasesNames) { + for (const alias of aliases) { operations.push( lambda.updateAlias({ FunctionName: functionName, - Name: alias, + Name: alias.physicalName, FunctionVersion: versionUpdate.Version, }), ); @@ -153,23 +140,13 @@ export async function isHotswappableLambdaFunctionChange( /** * Determines which changes to this Alias are hotswappable or not */ -function classifyAliasChanges(change: HotswappableChangeCandidate): ChangeHotswapResult { +function classifyAliasChanges(change: ResourceChange): ChangeHotswapResult { const ret: ChangeHotswapResult = []; const classifiedChanges = classifyChanges(change, ['FunctionVersion']); classifiedChanges.reportNonHotswappablePropertyChanges(ret); - const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); - if (namesOfHotswappableChanges.length > 0) { - ret.push({ - hotswappable: true, - resourceType: change.newValue.Type, - propsChanged: [], - service: 'lambda', - resourceNames: [], - apply: async (_sdk: SDK) => { - }, - }); - } + // we only want to report not hotswappable changes to aliases + // the actual alias update will happen if we change the function return ret; } @@ -180,7 +157,7 @@ function classifyAliasChanges(change: HotswappableChangeCandidate): ChangeHotswa * Returns `undefined` if the change is not hotswappable. */ async function evaluateLambdaFunctionProps( - hotswappablePropChanges: PropDiffs, + hotswappablePropChanges: Record>, runtime: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { @@ -369,38 +346,44 @@ async function versionsAndAliases(logicalId: string, evaluateCfnTemplate: Evalua // find all Lambda Aliases that reference the above Versions const aliasesReferencingVersions = flatMap(versionsReferencingFunction, v => evaluateCfnTemplate.findReferencesTo(v.LogicalId)); - // Limited set of updates per function - // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism - const aliasesNames = await Promise.all(aliasesReferencingVersions.map(a => - evaluateCfnTemplate.evaluateCfnExpression(a.Properties?.Name))); - return { versionsReferencingFunction, aliasesNames }; + return { versionsReferencingFunction, aliasesReferencingVersions }; } -/** - * Renders the string used in displaying Alias resource names that reference the specified Lambda Function - */ -async function renderAliases( +async function dependantResources( logicalId: string, + functionName: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate, - callbackfn: (value: any, index: number, array: any[]) => Promise, -): Promise { - const aliasesNames = (await versionsAndAliases(logicalId, evaluateCfnTemplate)).aliasesNames; +): Promise> { + const candidates = await versionsAndAliases(logicalId, evaluateCfnTemplate); // Limited set of updates per function // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism - return Promise.all(aliasesNames.map(callbackfn)); -} + const aliases = await Promise.all(candidates.aliasesReferencingVersions.map(async (a) => { + const name = await evaluateCfnTemplate.evaluateCfnExpression(a.Properties?.Name); + return { + logicalId: a.LogicalId, + physicalName: name, + resourceType: 'AWS::Lambda::Alias', + description: `Lambda Alias '${name}' for Function '${functionName}'`, + }; + })); -/** - * Renders the string used in displaying Version resource names that reference the specified Lambda Function - */ -async function renderVersions( - logicalId: string, - evaluateCfnTemplate: EvaluateCloudFormationTemplate, - versionString: string[], -): Promise { - const versions = (await versionsAndAliases(logicalId, evaluateCfnTemplate)).versionsReferencingFunction; + const versions = candidates.versionsReferencingFunction.map((v) => ( + { + logicalId: v.LogicalId, + resourceType: v.Type, + description: `Lambda Version for Function '${functionName}'`, + } + )); - return versions.length > 0 ? versionString : []; + return [ + ...versions, + ...aliases, + ]; } 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 1df6eb545..2800337c1 100644 --- a/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts @@ -1,4 +1,5 @@ -import type { ChangeHotswapResult, HotswappableChangeCandidate } from './common'; +import type { ChangeHotswapResult } from './common'; +import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import type { SDK } from '../aws-auth'; import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; @@ -6,18 +7,20 @@ import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation- * This means that the value is required to exist by CloudFormation's Custom Resource API (or our S3 Bucket Deployment Lambda's API) * but the actual value specified is irrelevant */ -export const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn'; +const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn'; + +const CDK_BUCKET_DEPLOYMENT_CFN_TYPE = 'Custom::CDKBucketDeployment'; export async function isHotswappableS3BucketDeploymentChange( _logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { // In old-style synthesis, the policy used by the lambda to copy assets Ref's the assets directly, // meaning that the changes made to the Policy are artifacts that can be safely ignored const ret: ChangeHotswapResult = []; - if (change.newValue.Type !== 'Custom::CDKBucketDeployment') { + if (change.newValue.Type !== CDK_BUCKET_DEPLOYMENT_CFN_TYPE) { return []; } @@ -27,19 +30,20 @@ export async function isHotswappableS3BucketDeploymentChange( ServiceToken: undefined, }); + // note that this gives the ARN of the lambda, not the name. This is fine though, the invoke() sdk call will take either + const functionName = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.ServiceToken); + if (!functionName) { + return ret; + } + ret.push({ + change: { + cause: change, + }, hotswappable: true, - resourceType: change.newValue.Type, - propsChanged: ['*'], service: 'custom-s3-deployment', resourceNames: [`Contents of S3 Bucket '${customResourceProperties.DestinationBucketName}'`], apply: async (sdk: SDK) => { - // note that this gives the ARN of the lambda, not the name. This is fine though, the invoke() sdk call will take either - const functionName = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.ServiceToken); - if (!functionName) { - return; - } - await sdk.lambda().invokeCommand({ FunctionName: functionName, // Lambda refuses to take a direct JSON object and requires it to be stringify()'d @@ -61,7 +65,7 @@ export async function isHotswappableS3BucketDeploymentChange( export async function skipChangeForS3DeployCustomResourcePolicy( iamPolicyLogicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { if (change.newValue.Type !== 'AWS::IAM::Policy') { 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 55cfa67bf..2344ae965 100644 --- a/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts +++ b/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts @@ -1,10 +1,11 @@ -import { type ChangeHotswapResult, classifyChanges, type HotswappableChangeCandidate } from './common'; +import { type ChangeHotswapResult, classifyChanges } from './common'; +import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import type { SDK } from '../aws-auth'; import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableStateMachineChange( logicalId: string, - change: HotswappableChangeCandidate, + change: ResourceChange, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { if (change.newValue.Type !== 'AWS::StepFunctions::StateMachine') { @@ -24,17 +25,20 @@ export async function isHotswappableStateMachineChange( stateMachineNameInCfnTemplate, }) : await evaluateCfnTemplate.findPhysicalNameFor(logicalId); + + // nothing to do + if (!stateMachineArn) { + return ret; + } + ret.push({ + change: { + cause: change, + }, hotswappable: true, - resourceType: change.newValue.Type, - propsChanged: namesOfHotswappableChanges, service: 'stepfunctions-service', resourceNames: [`${change.newValue.Type} '${stateMachineArn?.split(':')[6]}'`], apply: async (sdk: SDK) => { - if (!stateMachineArn) { - return; - } - // not passing the optional properties leaves them unchanged await sdk.stepFunctions().updateStateMachine({ stateMachineArn, diff --git a/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts index f6db9495f..484a9b866 100644 --- a/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts @@ -1,12 +1,12 @@ import { InvokeCommand } from '@aws-sdk/client-lambda'; import * as setup from '../_helpers/hotswap-test-setup'; import { HotswapMode } from '../../../lib/api/hotswap/common'; -import { REQUIRED_BY_CFN } from '../../../lib/api/hotswap/s3-bucket-deployments'; import { mockLambdaClient } from '../../util/mock-sdk'; import { silentTest } from '../../util/silent'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; +const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn'; const payloadWithoutCustomResProps = { RequestType: 'Update', ResponseURL: REQUIRED_BY_CFN,