Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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[];
}

/**
Expand All @@ -72,4 +84,3 @@ export interface HotswapDeployment {
*/
readonly mode: 'hotswap-only' | 'fall-back';
}

24 changes: 14 additions & 10 deletions packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -179,7 +181,7 @@ async function hotswapDeployment(
}

// apply the short-circuitable changes
await applyAllHotswappableChanges(sdk, ioSpan, hotswappableChanges);
await applyAllHotswappableChanges(sdk, ioSpan, hotswapOperations);

return {
stack,
Expand Down Expand Up @@ -225,7 +227,7 @@ async function classifyResourceChanges(
sdk,
hotswapPropertyOverrides,
);
hotswappableResources.push(...nestedHotswappableResources.hotswappableChanges);
hotswappableResources.push(...nestedHotswappableResources.hotswapOperations);
nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappableChanges);

continue;
Expand Down Expand Up @@ -275,7 +277,7 @@ async function classifyResourceChanges(
}

return {
hotswappableChanges: hotswappableResources,
hotswapOperations: hotswappableResources,
nonHotswappableChanges: nonHotswappableResources,
};
}
Expand Down Expand Up @@ -343,7 +345,7 @@ async function findNestedHotswappableChanges(
const nestedStack = nestedStackTemplates[logicalId];
if (!nestedStack.physicalName) {
return {
hotswappableChanges: [],
hotswapOperations: [],
nonHotswappableChanges: [
{
hotswappable: false,
Expand Down Expand Up @@ -470,8 +472,10 @@ async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan<any>, 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
Expand All @@ -488,8 +492,8 @@ async function applyHotswappableChange(sdk: SDK, ioSpan: IMessageSpan<any>, 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion packages/aws-cdk/lib/api/hotswap/code-build-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
9 changes: 2 additions & 7 deletions packages/aws-cdk/lib/api/hotswap/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -47,11 +47,6 @@ export interface HotswapOperation {
*/
readonly change: HotswappableChange;

/**
* The names of the resources being hotswapped.
*/
readonly resourceNames: string[];

/**
* Applies the hotswap operation
*/
Expand Down Expand Up @@ -80,7 +75,7 @@ export interface NonHotswappableChange {
export type ChangeHotswapResult = Array<HotswapOperation | NonHotswappableChange>;

export interface ClassifiedResourceChanges {
hotswappableChanges: HotswapOperation[];
hotswapOperations: HotswapOperation[];
nonHotswappableChanges: NonHotswappableChange[];
}

Expand Down
24 changes: 19 additions & 5 deletions packages/aws-cdk/lib/api/hotswap/ecs-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -141,6 +154,7 @@ export async function isHotswappableEcsServiceChange(
}

interface EcsService {
readonly logicalId: string;
readonly serviceArn: string;
}

Expand Down
21 changes: 14 additions & 7 deletions packages/aws-cdk/lib/api/hotswap/lambda-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>[] = [];
Expand Down Expand Up @@ -363,17 +368,19 @@ 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),
};
}));

const versions = candidates.versionsReferencingFunction.map((v) => (
{
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),
}
));

Expand Down
10 changes: 8 additions & 2 deletions packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChangeHotswapResult> {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down