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 d1dce5380..034d73586 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 @@ -70,6 +70,45 @@ export interface HotswappableChange { readonly resources: AffectedResource[]; } +export enum NonHotswappableReason { + /** + * Tags are not hotswappable + */ + TAGS = 'tags', + /** + * Changed resource properties are not hotswappable on this resource type + */ + PROPERTIES = 'properties', + /** + * A stack output has changed + */ + OUTPUT = 'output', + /** + * A dependant resource is not hotswappable + */ + DEPENDENCY_UNSUPPORTED = 'dependency-unsupported', + /** + * The resource type is not hotswappable + */ + RESOURCE_UNSUPPORTED = 'resource-unsupported', + /** + * The resource is created in the deployment + */ + RESOURCE_CREATION = 'resource-creation', + /** + * The resource is removed in the deployment + */ + RESOURCE_DELETION = 'resource-deletion', + /** + * The resource identified by the logical id has its type changed + */ + RESOURCE_TYPE_CHANGED = 'resource-type-changed', + /** + * The nested stack is created in the deployment + */ + NESTED_STACK_CREATION = 'nested-stack-creation', +} + /** * Information about a hotswap deployment */ diff --git a/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts b/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts index cc4d7ed0e..913ac837e 100644 --- a/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/deployments/hotswap-deployments.ts @@ -4,6 +4,7 @@ import type * as cxapi from '@aws-cdk/cx-api'; import type { WaiterResult } from '@smithy/util-waiter'; import * as chalk from 'chalk'; import type { AffectedResource, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads'; +import { NonHotswappableReason } 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'; @@ -77,7 +78,11 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = { return []; } - return reportNonHotswappableResource(change, 'This resource type is not supported for hotswap deployments'); + return reportNonHotswappableResource( + change, + NonHotswappableReason.RESOURCE_UNSUPPORTED, + 'This resource type is not supported for hotswap deployments', + ); }, 'AWS::CDK::Metadata': async () => [], @@ -210,7 +215,8 @@ async function classifyResourceChanges( for (const logicalId of Object.keys(stackChanges.outputs.changes)) { nonHotswappableResources.push({ hotswappable: false, - reason: 'output was changed', + reason: NonHotswappableReason.OUTPUT, + description: 'output was changed', logicalId, rejectedChanges: [], resourceType: 'Stack Output', @@ -253,6 +259,7 @@ async function classifyResourceChanges( reportNonHotswappableChange( nonHotswappableResources, hotswappableChangeCandidate, + NonHotswappableReason.RESOURCE_UNSUPPORTED, undefined, 'This resource type is not supported for hotswap deployments', ); @@ -350,7 +357,8 @@ async function findNestedHotswappableChanges( { hotswappable: false, logicalId, - reason: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`, + reason: NonHotswappableReason.NESTED_STACK_CREATION, + description: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`, rejectedChanges: [], resourceType: 'AWS::CloudFormation::Stack', }, @@ -425,7 +433,8 @@ function isCandidateForHotswapping( resourceType: change.newValue!.Type, logicalId, rejectedChanges: [], - reason: `resource '${logicalId}' was created by this deployment`, + reason: NonHotswappableReason.RESOURCE_CREATION, + description: `resource '${logicalId}' was created by this deployment`, }; } else if (!change.newValue) { return { @@ -433,7 +442,8 @@ function isCandidateForHotswapping( resourceType: change.oldValue!.Type, logicalId, rejectedChanges: [], - reason: `resource '${logicalId}' was destroyed by this deployment`, + reason: NonHotswappableReason.RESOURCE_DELETION, + description: `resource '${logicalId}' was destroyed by this deployment`, }; } @@ -444,7 +454,8 @@ function isCandidateForHotswapping( resourceType: change.newValue?.Type, logicalId, rejectedChanges: [], - reason: `resource '${logicalId}' had its type changed from '${change.oldValue?.Type}' to '${change.newValue?.Type}'`, + reason: NonHotswappableReason.RESOURCE_TYPE_CHANGED, + description: `resource '${logicalId}' had its type changed from '${change.oldValue?.Type}' to '${change.newValue?.Type}'`, }; } @@ -555,14 +566,14 @@ async function logNonHotswappableChanges( chalk.bold(change.logicalId), chalk.bold(change.resourceType), chalk.bold(change.rejectedChanges), - chalk.red(change.reason), + chalk.red(change.description), )); } else { messages.push(format( ' logicalID: %s, type: %s, reason: %s', chalk.bold(change.logicalId), chalk.bold(change.resourceType), - chalk.red(change.reason), + chalk.red(change.description), )); } } diff --git a/packages/aws-cdk/lib/api/hotswap/common.ts b/packages/aws-cdk/lib/api/hotswap/common.ts index 8be7cf6e8..3b7be7bec 100644 --- a/packages/aws-cdk/lib/api/hotswap/common.ts +++ b/packages/aws-cdk/lib/api/hotswap/common.ts @@ -1,6 +1,7 @@ import type { PropertyDifference } from '@aws-cdk/cloudformation-diff'; import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import type { HotswappableChange, ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; +import { NonHotswappableReason } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; import { ToolkitError } from '../../toolkit/error'; import type { SDK } from '../aws-auth'; @@ -58,11 +59,15 @@ export interface NonHotswappableChange { readonly resourceType: string; readonly rejectedChanges: Array; readonly logicalId: string; + /** + * Why was this change was deemed non-hotswappable + */ + readonly reason: NonHotswappableReason; /** * Tells the user exactly why this change was deemed non-hotswappable and what its logical ID is. - * If not specified, `reason` will be autofilled to state that the properties listed in `rejectedChanges` are not hotswappable. + * If not specified, `displayReason` default to state that the properties listed in `rejectedChanges` are not hotswappable. */ - readonly reason?: string; + readonly description?: string; /** * Whether or not to show this change when listing non-hotswappable changes in HOTSWAP_ONLY mode. Does not affect * listing in FALL_BACK mode. @@ -195,13 +200,15 @@ export class ClassifiedChanges { const nonHotswappablePropNames = Object.keys(this.nonHotswappableProps); if (nonHotswappablePropNames.length > 0) { const tagOnlyChange = nonHotswappablePropNames.length === 1 && nonHotswappablePropNames[0] === 'Tags'; + const reason = tagOnlyChange ? NonHotswappableReason.TAGS : NonHotswappableReason.PROPERTIES; + const description = tagOnlyChange ? 'Tags are not hotswappable' : `resource properties '${nonHotswappablePropNames}' are not hotswappable on this resource type`; + reportNonHotswappableChange( ret, this.change, + reason, this.nonHotswappableProps, - tagOnlyChange - ? 'Tags are not hotswappable' - : `resource properties '${nonHotswappablePropNames}' are not hotswappable on this resource type`, + description, ); } } @@ -229,27 +236,26 @@ export function classifyChanges(xs: ResourceChange, hotswappablePropNames: strin export function reportNonHotswappableChange( ret: ChangeHotswapResult, change: ResourceChange, + reason: NonHotswappableReason, nonHotswappableProps?: PropDiffs, - reason?: string, - hotswapOnlyVisible?: boolean, + description?: string, + hotswapOnlyVisible: boolean = true, ): void { - let hotswapOnlyVisibility = true; - if (hotswapOnlyVisible === false) { - hotswapOnlyVisibility = false; - } ret.push({ hotswappable: false, rejectedChanges: Object.keys(nonHotswappableProps ?? change.propertyUpdates), logicalId: change.logicalId, resourceType: change.newValue.Type, reason, - hotswapOnlyVisible: hotswapOnlyVisibility, + description: description, + hotswapOnlyVisible, }); } export function reportNonHotswappableResource( change: ResourceChange, - reason?: string, + reason: NonHotswappableReason, + description?: string, ): ChangeHotswapResult { return [ { @@ -258,6 +264,7 @@ export function reportNonHotswappableResource( logicalId: change.logicalId, resourceType: change.newValue.Type, reason, + description: description, }, ]; } diff --git a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts index 3e9d197a4..595bb4947 100644 --- a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts +++ b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts @@ -7,7 +7,7 @@ import { reportNonHotswappableChange, transformObjectKeys, } from './common'; -import type { ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads/hotswap'; +import { NonHotswappableReason, 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'; @@ -48,9 +48,21 @@ export async function isHotswappableEcsServiceChange( } } if (ecsServicesReferencingTaskDef.length === 0) { - // if there are no resources referencing the TaskDefinition, - // hotswap is not possible in FALL_BACK mode - reportNonHotswappableChange(ret, change, undefined, 'No ECS services reference the changed task definition', false); + /** + * ECS Services can have a task definition that doesn't refer to the task definition being updated. + * We have to log this as a non-hotswappable change to the task definition, but when we do, + * we wind up hotswapping the task definition and logging it as a non-hotswappable change. + * + * This logic prevents us from logging that change as non-hotswappable when we hotswap it. + */ + reportNonHotswappableChange( + ret, + change, + NonHotswappableReason.DEPENDENCY_UNSUPPORTED, + undefined, + 'No ECS services reference the changed task definition', + false, + ); } if (resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) { // if something besides an ECS Service is referencing the TaskDefinition, @@ -60,6 +72,7 @@ export async function isHotswappableEcsServiceChange( reportNonHotswappableChange( ret, change, + NonHotswappableReason.DEPENDENCY_UNSUPPORTED, undefined, `A resource '${taskRef.LogicalId}' with Type '${taskRef.Type}' that is not an ECS Service was found referencing the changed TaskDefinition '${logicalId}'`, );