@@ -3,7 +3,7 @@ import * as cfn_diff from '@aws-cdk/cloudformation-diff';
33import type * as cxapi from '@aws-cdk/cx-api' ;
44import type { WaiterResult } from '@smithy/util-waiter' ;
55import * as chalk from 'chalk' ;
6- import type { AffectedResource , ResourceChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads' ;
6+ import type { AffectedResource , HotswapResult , ResourceSubject , ResourceChange , NonHotswappableChange } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads' ;
77import { NonHotswappableReason } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/payloads' ;
88import type { IMessageSpan , IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private' ;
99import { IO , SPAN } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private' ;
@@ -21,7 +21,6 @@ import type {
2121 HotswapOperation ,
2222 RejectedChange ,
2323 HotswapPropertyOverrides ,
24- HotswapResult ,
2524} from '../hotswap/common' ;
2625import {
2726 ICON ,
@@ -156,22 +155,24 @@ async function hotswapDeployment(
156155 } ) ;
157156
158157 const stackChanges = cfn_diff . fullDiff ( currentTemplate . deployedRootTemplate , stack . template ) ;
159- const { hotswappable : hotswapOperations , nonHotswappable : nonHotswappableChanges } = await classifyResourceChanges (
158+ const { hotswappable, nonHotswappable } = await classifyResourceChanges (
160159 stackChanges ,
161160 evaluateCfnTemplate ,
162161 sdk ,
163162 currentTemplate . nestedStacks , hotswapPropertyOverrides ,
164163 ) ;
165164
166- await logNonHotswappableChanges ( ioSpan , nonHotswappableChanges , hotswapMode ) ;
165+ await logNonHotswappableChanges ( ioSpan , nonHotswappable , hotswapMode ) ;
167166
168- const hotswappableChanges = hotswapOperations . map ( o => o . change ) ;
167+ const hotswappableChanges = hotswappable . map ( o => o . change ) ;
168+ const nonHotswappableChanges = nonHotswappable . map ( n => n . change ) ;
169169
170170 // preserve classic hotswap behavior
171171 if ( hotswapMode === 'fall-back' ) {
172172 if ( nonHotswappableChanges . length > 0 ) {
173173 return {
174174 stack,
175+ mode : hotswapMode ,
175176 hotswapped : false ,
176177 hotswappableChanges,
177178 nonHotswappableChanges,
@@ -180,10 +181,11 @@ async function hotswapDeployment(
180181 }
181182
182183 // apply the short-circuitable changes
183- await applyAllHotswappableChanges ( sdk , ioSpan , hotswapOperations ) ;
184+ await applyAllHotswappableChanges ( sdk , ioSpan , hotswappable ) ;
184185
185186 return {
186187 stack,
188+ mode : hotswapMode ,
187189 hotswapped : true ,
188190 hotswappableChanges,
189191 nonHotswappableChanges,
@@ -214,10 +216,15 @@ async function classifyResourceChanges(
214216 for ( const logicalId of Object . keys ( stackChanges . outputs . changes ) ) {
215217 nonHotswappableResources . push ( {
216218 hotswappable : false ,
217- reason : NonHotswappableReason . OUTPUT ,
218- description : 'output was changed' ,
219- logicalId,
220- resourceType : 'Stack Output' ,
219+ change : {
220+ reason : NonHotswappableReason . OUTPUT ,
221+ description : 'output was changed' ,
222+ subject : {
223+ type : 'Output' ,
224+ logicalId,
225+ metadata : evaluateCfnTemplate . metadataFor ( logicalId ) ,
226+ } ,
227+ } ,
221228 } ) ;
222229 }
223230 // gather the results of the detector functions
@@ -237,7 +244,7 @@ async function classifyResourceChanges(
237244 continue ;
238245 }
239246
240- const hotswappableChangeCandidate = isCandidateForHotswapping ( change , logicalId ) ;
247+ const hotswappableChangeCandidate = isCandidateForHotswapping ( logicalId , change , evaluateCfnTemplate ) ;
241248 // we don't need to run this through the detector functions, we can already judge this
242249 if ( 'hotswappable' in hotswappableChangeCandidate ) {
243250 if ( ! hotswappableChangeCandidate . hotswappable ) {
@@ -348,10 +355,16 @@ async function findNestedHotswappableChanges(
348355 nonHotswappable : [
349356 {
350357 hotswappable : false ,
351- logicalId,
352- reason : NonHotswappableReason . NESTED_STACK_CREATION ,
353- 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` ,
354- resourceType : 'AWS::CloudFormation::Stack' ,
358+ change : {
359+ reason : NonHotswappableReason . NESTED_STACK_CREATION ,
360+ description : 'newly created nested stacks cannot be hotswapped' ,
361+ subject : {
362+ type : 'Resource' ,
363+ logicalId,
364+ resourceType : 'AWS::CloudFormation::Stack' ,
365+ metadata : evaluateCfnTemplate . metadataFor ( logicalId ) ,
366+ } ,
367+ } ,
355368 } ,
356369 ] ,
357370 } ;
@@ -414,36 +427,56 @@ function makeRenameDifference(
414427 * Returns a `NonHotswappableChange` if the change is not hotswappable
415428 */
416429function isCandidateForHotswapping (
417- change : cfn_diff . ResourceDifference ,
418430 logicalId : string ,
431+ change : cfn_diff . ResourceDifference ,
432+ evaluateCfnTemplate : EvaluateCloudFormationTemplate ,
419433) : RejectedChange | ResourceChange {
420434 // a resource has been removed OR a resource has been added; we can't short-circuit that change
421435 if ( ! change . oldValue ) {
422436 return {
423437 hotswappable : false ,
424- resourceType : change . newValue ! . Type ,
425- logicalId,
426- reason : NonHotswappableReason . RESOURCE_CREATION ,
427- description : `resource '${ logicalId } ' was created by this deployment` ,
438+ change : {
439+ reason : NonHotswappableReason . RESOURCE_CREATION ,
440+ description : `resource '${ logicalId } ' was created by this deployment` ,
441+ subject : {
442+ type : 'Resource' ,
443+ logicalId,
444+ resourceType : change . newValue ! . Type ,
445+ metadata : evaluateCfnTemplate . metadataFor ( logicalId ) ,
446+ } ,
447+ } ,
428448 } ;
429449 } else if ( ! change . newValue ) {
430450 return {
431451 hotswappable : false ,
432- resourceType : change . oldValue ! . Type ,
433452 logicalId,
434- reason : NonHotswappableReason . RESOURCE_DELETION ,
435- description : `resource '${ logicalId } ' was destroyed by this deployment` ,
453+ change : {
454+ reason : NonHotswappableReason . RESOURCE_DELETION ,
455+ description : `resource '${ logicalId } ' was destroyed by this deployment` ,
456+ subject : {
457+ type : 'Resource' ,
458+ logicalId,
459+ resourceType : change . oldValue . Type ,
460+ metadata : evaluateCfnTemplate . metadataFor ( logicalId ) ,
461+ } ,
462+ } ,
436463 } ;
437464 }
438465
439466 // a resource has had its type changed
440- if ( change . newValue ? .Type !== change . oldValue ? .Type ) {
467+ if ( change . newValue . Type !== change . oldValue . Type ) {
441468 return {
442469 hotswappable : false ,
443- resourceType : change . newValue ?. Type ,
444- logicalId,
445- reason : NonHotswappableReason . RESOURCE_TYPE_CHANGED ,
446- description : `resource '${ logicalId } ' had its type changed from '${ change . oldValue ?. Type } ' to '${ change . newValue ?. Type } '` ,
470+ change : {
471+ reason : NonHotswappableReason . RESOURCE_TYPE_CHANGED ,
472+ description : `resource '${ logicalId } ' had its type changed from '${ change . oldValue ?. Type } ' to '${ change . newValue ?. Type } '` ,
473+ subject : {
474+ type : 'Resource' ,
475+ logicalId,
476+ resourceType : change . newValue . Type ,
477+ metadata : evaluateCfnTemplate . metadataFor ( logicalId ) ,
478+ } ,
479+ } ,
447480 } ;
448481 }
449482
@@ -452,6 +485,7 @@ function isCandidateForHotswapping(
452485 oldValue : change . oldValue ,
453486 newValue : change . newValue ,
454487 propertyUpdates : change . propertyUpdates ,
488+ metadata : evaluateCfnTemplate . metadataFor ( logicalId ) ,
455489 } ;
456490}
457491
@@ -547,25 +581,51 @@ async function logNonHotswappableChanges(
547581 messages . push ( format ( '%s %s' , chalk . red ( '⚠️' ) , chalk . red ( 'The following non-hotswappable changes were found:' ) ) ) ;
548582 }
549583
550- for ( const change of nonHotswappableChanges ) {
551- if ( change . rejectedProperties ?. length ) {
552- messages . push ( format (
553- ' logicalID: %s, type: %s, rejected changes: %s, reason: %s' ,
554- chalk . bold ( change . logicalId ) ,
555- chalk . bold ( change . resourceType ) ,
556- chalk . bold ( change . rejectedProperties ) ,
557- chalk . red ( change . description ) ,
558- ) ) ;
559- } else {
560- messages . push ( format (
561- ' logicalID: %s, type: %s, reason: %s' ,
562- chalk . bold ( change . logicalId ) ,
563- chalk . bold ( change . resourceType ) ,
564- chalk . red ( change . description ) ,
565- ) ) ;
566- }
584+ for ( const rejection of nonHotswappableChanges ) {
585+ messages . push ( ' ' + nonHotswappableChangeMessage ( rejection . change ) ) ;
567586 }
568587 messages . push ( '' ) ; // newline
569588
570589 await ioSpan . notify ( IO . DEFAULT_TOOLKIT_INFO . msg ( messages . join ( '\n' ) ) ) ;
571590}
591+
592+ /**
593+ * Formats a NonHotswappableChange
594+ */
595+ function nonHotswappableChangeMessage ( change : NonHotswappableChange ) : string {
596+ const subject = change . subject ;
597+ const reason = change . description ?? change . reason ;
598+
599+ switch ( subject . type ) {
600+ case 'Output' :
601+ return format (
602+ 'output: %s, reason: %s' ,
603+ chalk . bold ( subject . logicalId ) ,
604+ chalk . red ( reason ) ,
605+ ) ;
606+ case 'Resource' :
607+ return nonHotswappableResourceMessage ( subject , reason ) ;
608+ }
609+ }
610+
611+ /**
612+ * Formats a non-hotswappable resource subject
613+ */
614+ function nonHotswappableResourceMessage ( subject : ResourceSubject , reason : string ) : string {
615+ if ( subject . rejectedProperties ?. length ) {
616+ return format (
617+ 'resource: %s, type: %s, rejected changes: %s, reason: %s' ,
618+ chalk . bold ( subject . logicalId ) ,
619+ chalk . bold ( subject . resourceType ) ,
620+ chalk . bold ( subject . rejectedProperties ) ,
621+ chalk . red ( reason ) ,
622+ ) ;
623+ }
624+
625+ return format (
626+ 'resource: %s, type: %s, reason: %s' ,
627+ chalk . bold ( subject . logicalId ) ,
628+ chalk . bold ( subject . resourceType ) ,
629+ chalk . red ( reason ) ,
630+ ) ;
631+ }
0 commit comments