"use strict" ; const path = require ( "path" ) ; const fs = require ( "fs-extra" ) ; const aws = require ( "aws-sdk" ) ; const chalk = require ( "chalk" ) ; const yaml = require ( "js-yaml" ) ; const spawn = require ( "cross-spawn" ) ; const { logger : rootLogger , getChildLogger , initializeLogger , } = require ( "./logger" ) ; const logger = getChildLogger ( "core" ) ; const cdkLogger = getChildLogger ( "cdk" ) ; const packageJson = require ( "../package.json" ) ; const { getHelperMessage } = require ( "./errorHelpers" ) ; const { makeCancelable } = require ( "./cancelablePromise" ) ; const STACK_DEPLOY_STATUS = { PENDING : "pending" , DEPLOYING : "deploying" , SUCCEEDED : "succeeded" , UNCHANGED : "unchanged" , FAILED : "failed" , SKIPPED : "skipped" , } ; const STACK_DESTROY_STATUS = { PENDING : "pending" , REMOVING : "removing" , SUCCEEDED : "succeeded" , FAILED : "failed" , SKIPPED : "skipped" , } ; function getCdkVersion ( ) { return packageJson . dependencies [ "aws-cdk" ] ; } function synth ( cdkOptions ) { logger . debug ( "synth" , cdkOptions ) ; const synthPromise = runCdkSynth ( cdkOptions ) ; const finalPromise = synthPromise . then ( ( ) => { return parseManifest ( cdkOptions ) ; } ) ; return makeCancelable ( finalPromise , ( ) => { synthPromise . cancel ( ) ; } ) ; } function runCdkSynth ( cdkOptions ) { let child ; const allStderrs = [ ] ; const promise = new Promise ( ( resolve , reject ) => { const context = [ ] ; ( cdkOptions . context || [ ] ) . forEach ( ( c ) => context . push ( "-c" , c ) ) ; child = spawn ( getCdkBinPath ( ) , [ "synth" , "--no-version-reporting" , "--app" , cdkOptions . app , "--output" , cdkOptions . output , "--quiet" , ... context , ... ( cdkOptions . roleArn ? [ "--role-arn" , cdkOptions . roleArn ] : [ ] ) , ... ( cdkOptions . noColor ? [ "--no-color" ] : [ ] ) , ... ( cdkOptions . verbose === 0 ? [ ] : [ "--verbose" ] ) , ] , { stdio : "pipe" , env : buildCDKSpawnEnv ( cdkOptions ) , } ) ; child . stdout . on ( "data" , ( data ) => { cdkLogger . trace ( data . toString ( ) ) ; process . stdout . write ( chalk . grey ( data ) ) ; } ) ; child . stderr . on ( "data" , ( data ) => { const dataStr = data . toString ( ) ; cdkLogger . trace ( dataStr ) ; if ( dataStr . startsWith ( "Subprocess exited with error 1" ) || dataStr . startsWith ( "Successfully synthesized to " ) || dataStr . startsWith ( "Supply a stack id " ) ) { return ; } process . stderr . write ( chalk . grey ( data ) ) ; allStderrs . push ( dataStr ) ; } ) ; child . on ( "exit" , function ( code ) { if ( code !== 0 ) { let errorHelper = getHelperMessage ( allStderrs . join ( "" ) ) ; errorHelper = errorHelper ? `\n${ errorHelper }\n` : errorHelper ; const error = new Error ( errorHelper || "There was an error synthesizing your app." ) ; error . stderr = allStderrs . join ( "" ) ; reject ( error ) ; } else { resolve ( code ) ; } } ) ; child . on ( "error" , function ( error ) { error . stderr = allStderrs . join ( "" ) ; cdkLogger . error ( error ) ; reject ( error ) ; } ) ; } ) ; return makeCancelable ( promise , ( ) => { child && child . kill ( ) ; } ) ; } async function diff ( cdkOptions , stackNames ) { logger . debug ( "diff" , cdkOptions ) ; const response = spawn . sync ( getCdkBinPath ( ) , [ "diff" , "--no-version-reporting" , "--app" , cdkOptions . app , "--output" , cdkOptions . output , ... ( cdkOptions . roleArn ? [ "--role-arn" , cdkOptions . roleArn ] : [ ] ) , ... ( cdkOptions . noColor ? [ "--no-color" ] : [ ] ) , ... ( cdkOptions . verbose === 0 ? [ ] : [ "--verbose" ] ) , ... ( stackNames ? [ ... stackNames ] : [ ] ) , ] , { stdio : "inherit" , env : buildCDKSpawnEnv ( cdkOptions ) , } ) ; if ( response . status !== 0 && response . status !== 1 ) { throw new Error ( "There was an error generating the diff." ) ; } return response ; } async function bootstrap ( cdkOptions ) { logger . debug ( "bootstrap" , cdkOptions ) ; const response = spawn . sync ( getCdkBinPath ( ) , [ "bootstrap" , "--no-version-reporting" , "--app" , cdkOptions . output , ... ( cdkOptions . roleArn ? [ "--role-arn" , cdkOptions . roleArn ] : [ ] ) , ... ( cdkOptions . noColor ? [ "--no-color" ] : [ ] ) , ... ( cdkOptions . verbose === 0 ? [ ] : [ "--verbose" ] ) , ] , { stdio : "inherit" , env : { ... buildCDKSpawnEnv ( cdkOptions ) , CDK_NEW_BOOTSTRAP : true , } , } ) ; if ( response . status !== 0 ) { throw new Error ( "There was an error bootstrapping your AWS account." ) ; } return response ; } async function deployInit ( cdkOptions , stackName ) { let { stacks } = await parseManifest ( cdkOptions ) ; if ( stackName ) { stacks = stacks . filter ( ( { name } ) => name === stackName ) ; if ( stacks . length === 0 ) { throw new Error ( `Stack ${ stackName } is not found in your app.` ) ; } stacks [ 0 ] . dependencies = [ ] ; } const stackStates = stacks . map ( ( { name , region , dependencies } ) => ( { name , status : STACK_DEPLOY_STATUS . PENDING , dependencies , account : undefined , region , startedAt : undefined , endedAt : undefined , events : [ ] , eventsFirstEventAt : undefined , errorMessage : undefined , outputs : undefined , } ) ) ; return { stackStates , isCompleted : false } ; } async function deployPoll ( cdkOptions , stackStates ) { const deployStacks = async ( ) => { let hasSucceededStack = false ; const statusesByStackName = { } ; stackStates . forEach ( ( { name , status } ) => { statusesByStackName [ name ] = status ; } ) ; await Promise . all ( stackStates . filter ( ( stackState ) => stackState . status === STACK_DEPLOY_STATUS . PENDING ) . filter ( ( stackState ) => stackState . dependencies . every ( ( dep ) => ! [ STACK_DEPLOY_STATUS . PENDING , STACK_DEPLOY_STATUS . DEPLOYING , ] . includes ( statusesByStackName [ dep ] ) ) ) . map ( async ( stackState ) => { try { const { status , statusReason , account , outputs , exports } = cdkOptions . deployStrategy === "CLOUDFORMATION" ? await deployStackTemplate ( cdkOptions , stackState ) : await deployStack ( cdkOptions , stackState ) ; stackState . status = status ; stackState . startedAt = Date . now ( ) ; stackState . account = account ; stackState . outputs = outputs ; stackState . exports = exports ; if ( status === STACK_DEPLOY_STATUS . DEPLOYING ) { } else if ( status === STACK_DEPLOY_STATUS . UNCHANGED ) { stackState . endedAt = stackState . startedAt ; hasSucceededStack = true ; logger . info ( chalk . green ( `\n ✅  ${ stackState . name } (no changes)\n` ) ) ; } else if ( status === STACK_DEPLOY_STATUS . FAILED && statusReason === "no_resources" ) { stackState . endedAt = stackState . startedAt ; stackState . errorMessage = `The ${ stackState . name } stack contains no resources.` ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ stackState . errorMessage }\n` ) ) ; } else if ( status === STACK_DEPLOY_STATUS . FAILED && statusReason === "not_bootstrapped" ) { stackState . status = STACK_DEPLOY_STATUS . PENDING ; try { await bootstrap ( cdkOptions ) ; } catch ( bootstrapEx ) { logger . debug ( `Bootstrap stack ${ stackState . name } exception ${ bootstrapEx }` ) ; if ( isRetryableException ( bootstrapEx ) ) { } else { stackState . status = STACK_DEPLOY_STATUS . FAILED ; stackState . startedAt = Date . now ( ) ; stackState . endedAt = stackState . startedAt ; stackState . errorMessage = bootstrapEx . message ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ bootstrapEx }\n` ) ) ; } } } else { stackState . endedAt = stackState . startedAt ; stackState . errorMessage = `The ${ stackState . name } stack failed to deploy.` ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ stackState . errorMessage }\n` ) ) ; } } catch ( deployEx ) { logger . debug ( `Deploy stack ${ stackState . name } exception ${ deployEx }` ) ; if ( isRetryableException ( deployEx ) ) { } else { stackState . status = STACK_DEPLOY_STATUS . FAILED ; stackState . startedAt = Date . now ( ) ; stackState . endedAt = stackState . startedAt ; stackState . errorMessage = deployEx . message ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ deployEx }\n` ) ) ; } } } ) ) ; if ( hasSucceededStack ) { logger . debug ( "At least 1 stack successfully deployed, call deployStacks() again" ) ; await deployStacks ( ) ; } } ; const updateDeployStatuses = async ( ) => { await Promise . all ( stackStates . filter ( ( stackState ) => stackState . status === STACK_DEPLOY_STATUS . DEPLOYING ) . map ( async ( stackState ) => { try { logger . debug ( `Fetching stack events ${ stackState . name }` ) ; await getStackEvents ( stackState ) ; } catch ( e ) { logger . debug ( e ) ; if ( isRetryableException ( e ) ) { return ; } } try { logger . debug ( `Checking stack status ${ stackState . name }` ) ; const { isDeployed , outputs , exports } = await getDeployStatus ( stackState ) ; stackState . outputs = outputs ; stackState . exports = exports ; if ( isDeployed ) { stackState . status = STACK_DEPLOY_STATUS . SUCCEEDED ; stackState . endedAt = Date . now ( ) ; logger . info ( chalk . green ( `\n ✅  ${ stackState . name }\n` ) ) ; } } catch ( statusEx ) { logger . debug ( statusEx ) ; if ( isRetryableException ( statusEx ) ) { } else { stackState . status = STACK_DEPLOY_STATUS . FAILED ; stackState . endedAt = Date . now ( ) ; stackState . errorMessage = getErrorMessageFromEvents ( stackState . events ) || statusEx . message ; stackState . errorHelper = getHelperMessage ( stackState . errorMessage ) ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ stackState . errorMessage }\n` ) ) ; } } } ) ) ; } ; const skipPendingStacks = ( ) => { stackStates . filter ( ( stackState ) => stackState . status === STACK_DEPLOY_STATUS . PENDING ) . forEach ( ( stackState ) => { stackState . status = STACK_DEPLOY_STATUS . SKIPPED ; } ) ; } ; const getDeployStatus = async ( stackState ) => { const stackName = stackState . name ; const cfn = new aws . CloudFormation ( { region : stackState . region } ) ; const ret = await cfn . describeStacks ( { StackName : stackName } ) . promise ( ) ; if ( ret . Stacks . length === 0 ) { throw new Error ( `Stack ${ stackName } failed to deploy, it is removed while deploying.` ) ; } const { StackStatus , Outputs } = ret . Stacks [ 0 ] ; if ( StackStatus . endsWith ( "_IN_PROGRESS" ) ) { return { isDeployed : false } ; } if ( StackStatus === "ROLLBACK_COMPLETE" || StackStatus === "ROLLBACK_FAILED" ) { throw new Error ( `Stack ${ stackName } failed creation, it may need to be manually deleted from the AWS console: ${ StackStatus }` ) ; } if ( StackStatus !== "CREATE_COMPLETE" && StackStatus !== "UPDATE_COMPLETE" ) { throw new Error ( `Stack ${ stackName } failed to deploy: ${ StackStatus }` ) ; } const outputs = { } ; const exports = { } ; ( Outputs || [ ] ) . forEach ( ( { OutputKey , OutputValue , ExportName } ) => { OutputKey && ( outputs [ OutputKey ] = OutputValue ) ; ExportName && ( exports [ ExportName ] = OutputValue ) ; } ) ; return { isDeployed : true , outputs , exports } ; } ; const getStackEvents = async ( stackState ) => { const cfn = new aws . CloudFormation ( { region : stackState . region } ) ; const ret = await cfn . describeStackEvents ( { StackName : stackState . name } ) . promise ( ) ; const stackEvents = ret . StackEvents || [ ] ; if ( ! stackState . eventsFirstEventAt ) { const firstRelevantEvent = stackEvents . find ( ( event ) => { const isStack = "AWS::CloudFormation::Stack" ; const updateIsInProgress = "UPDATE_IN_PROGRESS" ; const createIsInProgress = "CREATE_IN_PROGRESS" ; const deleteIsInProgress = "DELETE_IN_PROGRESS" ; return ( event . ResourceType === isStack && ( event . ResourceStatus === updateIsInProgress || event . ResourceStatus === createIsInProgress || event . ResourceStatus === deleteIsInProgress ) ) ; } ) ; if ( firstRelevantEvent ) { const eventDate = new Date ( firstRelevantEvent . Timestamp ) ; const updatedDate = eventDate . setSeconds ( eventDate . getSeconds ( ) - 5 ) ; stackState . eventsFirstEventAt = new Date ( updatedDate ) ; } } const events = stackState . events || [ ] ; if ( stackState . eventsFirstEventAt ) { const eventsFirstEventAtTs = Date . parse ( stackState . eventsFirstEventAt ) ; stackEvents . reverse ( ) . forEach ( ( event ) => { const eventInRange = eventsFirstEventAtTs <= Date . parse ( event . Timestamp ) ; if ( ! eventInRange ) { return ; } const eventNotLogged = events . every ( ( loggedEvent ) => loggedEvent . eventId !== event . EventId ) ; if ( ! eventNotLogged ) { return ; } printStackEvent ( stackState . name , event ) ; events . push ( { eventId : event . EventId , timestamp : event . Timestamp , resourceType : event . ResourceType , resourceStatus : event . ResourceStatus , resourceStatusReason : event . ResourceStatusReason , logicalResourceId : event . LogicalResourceId , } ) ; } ) ; } stackState . events = events ; } ; const isStatusCompleted = ( status ) => { return ! [ STACK_DEPLOY_STATUS . PENDING , STACK_DEPLOY_STATUS . DEPLOYING , ] . includes ( status ) ; } ; logger . trace ( `Initial stack states: ${ JSON . stringify ( stackStates ) }` ) ; await updateDeployStatuses ( ) ; logger . trace ( `After update deploy statuses: ${ JSON . stringify ( stackStates ) }` ) ; await deployStacks ( ) ; logger . trace ( `After deploy stacks: ${ JSON . stringify ( stackStates ) }` ) ; return { stackStates , isCompleted : stackStates . every ( ( { status } ) => isStatusCompleted ( status ) ) , } ; } async function deployStack ( cdkOptions , stackState ) { const { name : stackName , region } = stackState ; logger . debug ( "deploy stack: started" , stackName ) ; logger . debug ( "deploy stack: get pre-deploy status" ) ; let stackRet ; let stackLastUpdatedTime = 0 ; try { stackRet = await describeStackWithRetry ( { stackName , region } ) ; const { StackStatus , LastUpdatedTime } = stackRet . Stacks [ 0 ] ; logger . debug ( "deploy stack: get pre-deploy status:" , { StackStatus , LastUpdatedTime , } ) ; if ( StackStatus . endsWith ( "_IN_PROGRESS" ) ) { throw new Error ( `Stack ${ stackName } is in the ${ StackStatus } state. It cannot be deployed.` ) ; } stackLastUpdatedTime = LastUpdatedTime ? Date . parse ( LastUpdatedTime ) : 0 ; } catch ( e ) { if ( isStackNotExistException ( e ) ) { logger . debug ( "deploy stack: get pre-deploy status: stack does not exist" ) ; } else { logger . debug ( "deploy stack: get pre-deploy status: caught exception" ) ; logger . error ( e ) ; throw e ; } } logger . debug ( "deploy stack: check template changed" ) ; if ( stackRet && [ "CREATE_COMPLETE" , "UPDATE_COMPLETE" ] . includes ( stackRet . Stacks [ 0 ] . StackStatus ) ) { try { const templateRet = await getStackTemplateWithRetry ( { stackName , region , } ) ; const existingTemplateYml = yaml . dump ( yaml . load ( templateRet . TemplateBody ) ) ; const newTemplate = await getLocalTemplate ( cdkOptions , stackName ) ; const newTemplateYml = yaml . dump ( yaml . load ( newTemplate ) ) ; logger . debug ( existingTemplateYml ) ; logger . debug ( newTemplateYml ) ; if ( existingTemplateYml && newTemplateYml && existingTemplateYml === newTemplateYml ) { logger . debug ( "deploy stack: check template changed: unchanged" ) ; return buildDeployResponse ( { stackName , stackRet , status : STACK_DEPLOY_STATUS . UNCHANGED , } ) ; } } catch ( e ) { logger . debug ( "deploy stack: check template changed: caught exception" , e ) ; } } logger . debug ( "deploy stack: run cdk deploy" ) ; let cpCode ; let cpStdChunks = [ ] ; const args = [ "deploy" , stackName , "--no-version-reporting" , "--app" , cdkOptions . output , "--rollback" , cdkOptions . rollback === true ? "true" : "false" , "--exclusively" , "--require-approval" , "never" , "--execute" , "true" , ... ( cdkOptions . roleArn ? [ "--role-arn" , cdkOptions . roleArn ] : [ ] ) , ... ( cdkOptions . noColor ? [ "--no-color" ] : [ ] ) , ... ( cdkOptions . verbose === 0 ? [ ] : [ "--verbose" ] ) , ] ; const cp = spawn ( getCdkBinPath ( ) , args , { stdio : "pipe" , env : buildCDKSpawnEnv ( cdkOptions ) , } ) ; cp . stdout . on ( "data" , ( data ) => { cpStdChunks . push ( { stream : process . stdout , data } ) ; logger . trace ( "deploy stack: run cdk deploy: stdout:" , data . toString ( ) ) ; } ) ; cp . stderr . on ( "data" , ( data ) => { cpStdChunks . push ( { stream : process . stderr , data } ) ; logger . trace ( "deploy stack: run cdk deploy: stderr:" , data . toString ( ) ) ; } ) ; cp . on ( "close" , ( code ) => { logger . debug ( "deploy stack: run cdk deploy: exited with code" , code ) ; cpCode = code ; } ) ; logger . debug ( "deploy stack:" , "poll stack status" ) ; let cfUpdateWillStart = false ; let cfUpdateStarted = false ; let waitForCp = true ; do { try { stackRet = await describeStackWithRetry ( { stackName , region } ) ; const { StackStatus , LastUpdatedTime } = stackRet . Stacks [ 0 ] ; logger . debug ( "deploy stack: poll stack status:" , { StackStatus , LastUpdatedTime , } ) ; if ( StackStatus === "REVIEW_IN_PROGRESS" ) { cfUpdateWillStart = true ; } else if ( StackStatus === "DELETE_IN_PROGRESS" ) { cfUpdateWillStart = true ; } else { cfUpdateWillStart = false ; cfUpdateStarted = StackStatus . endsWith ( "_IN_PROGRESS" ) || ( LastUpdatedTime && Date . parse ( LastUpdatedTime ) > stackLastUpdatedTime ) ; } } catch ( e ) { if ( isStackNotExistException ( e ) ) { } else { logger . debug ( "deploy stack: poll stack status: caught exception" ) ; logger . error ( e ) ; throw e ; } } if ( cfUpdateWillStart ) { logger . debug ( "deploy stack: poll stack status: cf update will start" ) ; await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ; } else if ( cfUpdateStarted ) { logger . debug ( "deploy stack: poll stack status: cf update started" ) ; cp . kill ( ) ; waitForCp = false ; } else if ( cpCode !== undefined ) { logger . debug ( "deploy stack: poll stack status: cp exited" ) ; waitForCp = false ; } else { logger . debug ( "deploy stack: poll stack status: unknown" ) ; await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ; } } while ( waitForCp ) ; let status , statusReason ; if ( cfUpdateStarted ) { status = STACK_DEPLOY_STATUS . DEPLOYING ; } else if ( cpCode === 0 ) { if ( stackRet ) { status = STACK_DEPLOY_STATUS . UNCHANGED ; } else { status = STACK_DEPLOY_STATUS . FAILED ; statusReason = "no_resources" ; } } else { status = STACK_DEPLOY_STATUS . FAILED ; const stdOutput = cpStdChunks . map ( ( { data } ) => data . toString ( ) ) . join ( "" ) ; if ( stdOutput . indexOf ( "Has the environment been bootstrapped? Please run 'cdk bootstrap'" ) > - 1 ) { statusReason = "not_bootstrapped" ; } else { cpStdChunks . forEach ( ( { stream , data } ) => stream . write ( data ) ) ; } } return buildDeployResponse ( { stackName , stackRet , status , statusReason } ) ; } async function deployStackTemplate ( cdkOptions , stackState ) { const { name : stackName , region } = stackState ; const cfn = new aws . CloudFormation ( { region } ) ; logger . debug ( "deploy stack template: started" , stackName ) ; logger . debug ( "deploy stack template: get pre-deploy status" ) ; let stackRet ; try { stackRet = await describeStackWithRetry ( { stackName , region } ) ; const { StackStatus , LastUpdatedTime } = stackRet . Stacks [ 0 ] ; logger . debug ( "deploy stack template: get pre-deploy status:" , { StackStatus , LastUpdatedTime , } ) ; if ( StackStatus . endsWith ( "_IN_PROGRESS" ) ) { throw new Error ( `Stack ${ stackName } is in the ${ StackStatus } state. It cannot be deployed.` ) ; } } catch ( e ) { if ( isStackNotExistException ( e ) ) { } else { logger . debug ( "deploy stack template: get pre-deploy status: caught exception" ) ; logger . error ( e ) ; throw e ; } } let noChanges = false ; const commandFilePath = path . join ( cdkOptions . output , `${ stackName }.command` ) ; const hasCommandData = await fs . existsSync ( commandFilePath ) ; if ( hasCommandData ) { const commandData = await fs . readJson ( commandFilePath ) ; if ( commandData . isUpdate ) { try { await cfn . updateStack ( commandData . params ) . promise ( ) ; } catch ( e ) { if ( e . code === "ValidationError" && e . message === "No updates are to be performed." ) { noChanges = true ; } else { logger . debug ( "deploy stack template: get updateStack status: caught exception" ) ; logger . error ( e ) ; throw e ; } } } else { await cfn . createStack ( commandData . params ) . promise ( ) ; } } else { noChanges = true ; } let status , statusReason ; try { stackRet = await describeStackWithRetry ( { stackName , region } ) ; } catch ( e ) { if ( isStackNotExistException ( e ) ) { } else { logger . debug ( "deploy stack template: get post-deploy status: caught exception" ) ; logger . error ( e ) ; throw e ; } } if ( ! stackRet ) { status = STACK_DEPLOY_STATUS . FAILED ; statusReason = "no_resources" ; } else if ( noChanges ) { status = STACK_DEPLOY_STATUS . UNCHANGED ; } else { status = STACK_DEPLOY_STATUS . DEPLOYING ; } return buildDeployResponse ( { stackName , stackRet , status , statusReason } ) ; } function buildDeployResponse ( { stackName , stackRet , status , statusReason } ) { let account , outputs , exports ; if ( stackRet ) { const { StackId , Outputs } = stackRet . Stacks [ 0 ] ; const StackIdParts = StackId . split ( ":" ) ; account = StackIdParts [ 4 ] ; outputs = { } ; exports = { } ; Outputs . forEach ( ( { OutputKey , OutputValue , ExportName } ) => { outputs [ OutputKey ] = OutputValue ; if ( ExportName ) { exports [ ExportName ] = OutputValue ; } } ) ; } logger . debug ( "deploy stack:" , "done" , stackName , { status , statusReason , account , outputs , exports , } ) ; return { status , statusReason , account , outputs , exports } ; } async function destroyInit ( cdkOptions , stackName ) { let { stacks } = await parseManifest ( cdkOptions ) ; if ( stackName ) { stacks = stacks . filter ( ( { name } ) => name === stackName ) ; if ( stacks . length === 0 ) { throw new Error ( `Stack ${ stackName } is not found in your app.` ) ; } stacks [ 0 ] . dependencies = [ ] ; } const reverseDependencyMapping = { } ; stacks . forEach ( ( { name , dependencies } ) => dependencies . forEach ( ( dep ) => { reverseDependencyMapping [ dep ] = reverseDependencyMapping [ dep ] || [ ] ; reverseDependencyMapping [ dep ] . push ( name ) ; } ) ) ; const stackStates = stacks . map ( ( { name , region } ) => ( { name , status : STACK_DESTROY_STATUS . PENDING , dependencies : reverseDependencyMapping [ name ] || [ ] , region , events : [ ] , eventsFirstEventAt : undefined , errorMessage : undefined , } ) ) ; return { stackStates , isCompleted : false } ; } async function destroyPoll ( cdkOptions , stackStates ) { const destroyStacks = async ( ) => { let hasSucceededStack = false ; const statusesByStackName = { } ; stackStates . forEach ( ( { name , status } ) => { statusesByStackName [ name ] = status ; } ) ; await Promise . all ( stackStates . filter ( ( stackState ) => stackState . status === STACK_DESTROY_STATUS . PENDING ) . filter ( ( stackState ) => stackState . dependencies . every ( ( dep ) => statusesByStackName [ dep ] === STACK_DESTROY_STATUS . SUCCEEDED ) ) . map ( async ( stackState ) => { try { const { status } = cdkOptions . destroyStrategy === "CLOUDFORMATION" ? await destroyStackTemplate ( cdkOptions , stackState ) : await destroyStack ( cdkOptions , stackState ) ; stackState . status = status ; if ( status === STACK_DESTROY_STATUS . REMOVING ) { } else if ( status === STACK_DESTROY_STATUS . SUCCEEDED ) { hasSucceededStack = true ; logger . info ( chalk . green ( `\n ✅  ${ stackState . name }\n` ) ) ; } else { stackState . errorMessage = `The ${ stackState . name } stack failed to destroy.` ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ stackState . errorMessage }\n` ) ) ; } } catch ( e ) { logger . debug ( `Destroy stack ${ stackState . name } exception ${ e }` ) ; if ( isRetryableException ( e ) ) { } else { stackState . status = STACK_DESTROY_STATUS . FAILED ; stackState . errorMessage = e . message ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ e }\n` ) ) ; } } } ) ) ; if ( hasSucceededStack ) { logger . debug ( "At least 1 stack successfully destroyed, call destroyStacks() again" ) ; await destroyStacks ( ) ; } } ; const updateDestroyStatuses = async ( ) => { await Promise . all ( stackStates . filter ( ( stackState ) => stackState . status === STACK_DESTROY_STATUS . REMOVING ) . map ( async ( stackState ) => { try { logger . debug ( `Fetching stack events ${ stackState . name }` ) ; await getStackEvents ( stackState ) ; } catch ( eventsEx ) { logger . debug ( eventsEx ) ; if ( isRetryableException ( eventsEx ) ) { return ; } else if ( isStackNotExistException ( eventsEx ) ) { stackState . status = STACK_DESTROY_STATUS . SUCCEEDED ; logger . info ( chalk . green ( `\n ✅  ${ stackState . name }\n` ) ) ; return ; } } try { logger . debug ( `Checking stack status ${ stackState . name }` ) ; const { isDestroyed } = await getDestroyStatus ( stackState ) ; if ( isDestroyed ) { stackState . status = STACK_DESTROY_STATUS . SUCCEEDED ; logger . info ( chalk . green ( `\n ✅  ${ stackState . name }\n` ) ) ; } } catch ( statusEx ) { logger . debug ( statusEx ) ; if ( isRetryableException ( statusEx ) ) { } else if ( isStackNotExistException ( statusEx ) ) { stackState . status = STACK_DESTROY_STATUS . SUCCEEDED ; logger . info ( chalk . green ( `\n ✅  ${ stackState . name }\n` ) ) ; } else { stackState . status = STACK_DESTROY_STATUS . FAILED ; stackState . errorMessage = getErrorMessageFromEvents ( stackState . events ) || statusEx . message ; skipPendingStacks ( ) ; logger . info ( chalk . red ( `\n ❌  ${ chalk . bold ( stackState . name ) } failed: ${ stackState . errorMessage }\n` ) ) ; } } } ) ) ; } ; const skipPendingStacks = ( ) => { stackStates . filter ( ( stackState ) => stackState . status === STACK_DESTROY_STATUS . PENDING ) . forEach ( ( stackState ) => { stackState . status = STACK_DESTROY_STATUS . SKIPPED ; } ) ; } ; const getDestroyStatus = async ( stackState ) => { let isDestroyed ; const stackName = stackState . name ; const cfn = new aws . CloudFormation ( { region : stackState . region } ) ; const ret = await cfn . describeStacks ( { StackName : stackName } ) . promise ( ) ; if ( ret . Stacks . length === 0 ) { isDestroyed = true ; } else { const { StackStatus } = ret . Stacks [ 0 ] ; if ( StackStatus . endsWith ( "_IN_PROGRESS" ) ) { isDestroyed = false ; } else if ( StackStatus === "DELETE_COMPLETE" ) { isDestroyed = true ; } else { throw new Error ( `Stack ${ stackName } failed to destroy: ${ StackStatus }` ) ; } } return { isDestroyed } ; } ; const getStackEvents = async ( stackState ) => { const cfn = new aws . CloudFormation ( { region : stackState . region } ) ; const ret = await cfn . describeStackEvents ( { StackName : stackState . name } ) . promise ( ) ; const stackEvents = ret . StackEvents || [ ] ; if ( ! stackState . eventsFirstEventAt ) { const firstRelevantEvent = stackEvents . find ( ( event ) => { const isStack = "AWS::CloudFormation::Stack" ; const updateIsInProgress = "UPDATE_IN_PROGRESS" ; const createIsInProgress = "CREATE_IN_PROGRESS" ; const deleteIsInProgress = "DELETE_IN_PROGRESS" ; return ( event . ResourceType === isStack && ( event . ResourceStatus === updateIsInProgress || event . ResourceStatus === createIsInProgress || event . ResourceStatus === deleteIsInProgress ) ) ; } ) ; if ( firstRelevantEvent ) { const eventDate = new Date ( firstRelevantEvent . Timestamp ) ; const updatedDate = eventDate . setSeconds ( eventDate . getSeconds ( ) - 5 ) ; stackState . eventsFirstEventAt = new Date ( updatedDate ) ; } } const events = stackState . events || [ ] ; if ( stackState . eventsFirstEventAt ) { const eventsFirstEventAtTs = Date . parse ( stackState . eventsFirstEventAt ) ; stackEvents . reverse ( ) . forEach ( ( event ) => { const eventInRange = eventsFirstEventAtTs <= event . Timestamp ; if ( ! eventInRange ) { return ; } const eventNotLogged = events . every ( ( loggedEvent ) => loggedEvent . eventId !== event . EventId ) ; if ( ! eventNotLogged ) { return ; } printStackEvent ( stackState . name , event ) ; events . push ( { eventId : event . EventId , timestamp : event . Timestamp , resourceType : event . ResourceType , resourceStatus : event . ResourceStatus , resourceStatusReason : event . ResourceStatusReason , logicalResourceId : event . LogicalResourceId , } ) ; } ) ; } stackState . events = events ; } ; const isStatusCompleted = ( status ) => { return ! [ STACK_DESTROY_STATUS . PENDING , STACK_DESTROY_STATUS . REMOVING , ] . includes ( status ) ; } ; logger . trace ( `Initial stack states: ${ JSON . stringify ( stackStates ) }` ) ; await updateDestroyStatuses ( ) ; logger . trace ( `After update destroy statuses: ${ JSON . stringify ( stackStates ) }` ) ; await destroyStacks ( ) ; logger . trace ( `After destroy stacks: ${ JSON . stringify ( stackStates ) }` ) ; return { stackStates , isCompleted : stackStates . every ( ( { status } ) => isStatusCompleted ( status ) ) , } ; } async function destroyStack ( cdkOptions , stackState ) { const { name : stackName , region } = stackState ; logger . debug ( "destroy stack:" , "started" , stackName ) ; logger . debug ( "destroy stack:" , "get pre-deploy status" ) ; let stackLastUpdatedTime = 0 ; try { const stackRet = await describeStackWithRetry ( { stackName , region } ) ; const { StackStatus , LastUpdatedTime } = stackRet . Stacks [ 0 ] ; if ( StackStatus . endsWith ( "_IN_PROGRESS" ) ) { throw new Error ( `Stack ${ stackName } is in the ${ StackStatus } state. It cannot be destroyed.` ) ; } stackLastUpdatedTime = LastUpdatedTime ? Date . parse ( LastUpdatedTime ) : 0 ; } catch ( e ) { if ( isStackNotExistException ( e ) ) { return { status : STACK_DESTROY_STATUS . SUCCEEDED } ; } else { logger . error ( e ) ; throw e ; } } logger . debug ( "destroy stack:" , "run cdk destroy" ) ; let cpCode ; let cpStdChunks = [ ] ; const cp = spawn ( getCdkBinPath ( ) , [ "destroy" , stackName , "--no-version-reporting" , "--app" , cdkOptions . output , "--force" , "--exclusively" , "--execute" , "true" , ... ( cdkOptions . roleArn ? [ "--role-arn" , cdkOptions . roleArn ] : [ ] ) , ... ( cdkOptions . noColor ? [ "--no-color" ] : [ ] ) , ... ( cdkOptions . verbose === 0 ? [ ] : [ "--verbose" ] ) , ] , { stdio : "pipe" , env : buildCDKSpawnEnv ( cdkOptions ) , } ) ; cp . stdout . on ( "data" , ( data ) => cpStdChunks . push ( { stream : process . stdout , data } ) ) ; cp . stderr . on ( "data" , ( data ) => cpStdChunks . push ( { stream : process . stderr , data } ) ) ; cp . on ( "close" , ( code ) => { logger . debug ( `cdk destroy exited with code ${ code }` ) ; cpCode = code ; } ) ; logger . debug ( "destroy stack:" , "poll stack status" ) ; let stackRet ; let cfUpdateStarted = false ; let waitForCp = true ; do { try { stackRet = await describeStackWithRetry ( { stackName , region } ) ; const { StackStatus , LastUpdatedTime } = stackRet . Stacks [ 0 ] ; cfUpdateStarted = ( StackStatus . endsWith ( "_IN_PROGRESS" ) && StackStatus !== "REVIEW_IN_PROGRESS" ) || ( LastUpdatedTime && Date . parse ( LastUpdatedTime ) > stackLastUpdatedTime ) ; } catch ( e ) { if ( isStackNotExistException ( e ) ) { return { status : STACK_DESTROY_STATUS . SUCCEEDED } ; } else { logger . error ( e ) ; throw e ; } } if ( cfUpdateStarted ) { cp . kill ( ) ; waitForCp = false ; } else if ( cpCode !== undefined ) { waitForCp = false ; } else { await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ; } } while ( waitForCp ) ; let status ; if ( cfUpdateStarted ) { status = STACK_DESTROY_STATUS . REMOVING ; } else if ( cpCode === 0 ) { status = STACK_DESTROY_STATUS . SUCCEEDED ; } else { status = STACK_DESTROY_STATUS . FAILED ; cpStdChunks . forEach ( ( { stream , data } ) => stream . write ( data ) ) ; } logger . debug ( "destroy stack:" , "done" , stackName , { status } ) ; return { status } ; } async function destroyStackTemplate ( cdkOptions , stackState ) { const { name : stackName , region } = stackState ; const cfn = new aws . CloudFormation ( { region } ) ; logger . debug ( "destroy stack template:" , "started" , stackName ) ; logger . debug ( "destroy stack template:" , "get pre-destroy status" ) ; try { const stackRet = await describeStackWithRetry ( { stackName , region } ) ; const { StackStatus , LastUpdatedTime } = stackRet . Stacks [ 0 ] ; logger . debug ( "destroy stack template: get pre-destroy status:" , { StackStatus , LastUpdatedTime , } ) ; if ( StackStatus . endsWith ( "_IN_PROGRESS" ) ) { throw new Error ( `Stack ${ stackName } is in the ${ StackStatus } state. It cannot be destroyed.` ) ; } } catch ( e ) { if ( isStackNotExistException ( e ) ) { return { status : STACK_DESTROY_STATUS . SUCCEEDED } ; } else { logger . debug ( "destroy stack template: get pre-destroy destroy: caught exception" ) ; logger . error ( e ) ; throw e ; } } logger . debug ( "destroy stack template:" , "run deleteStack" ) ; try { await cfn . deleteStack ( { StackName : stackName } ) . promise ( ) ; } catch ( e ) { logger . debug ( "destroy stack template: get deleteStack status: caught exception" ) ; logger . error ( e ) ; throw e ; } const status = STACK_DESTROY_STATUS . REMOVING ; logger . debug ( "destroy stack template:" , "done" , stackName , { status } ) ; return { status } ; } /**
 * Finds the path to the CDK package executable by converting the file path of:
 * /Users/spongebob/serverless-stack/node_modules/aws-cdk/package.json
 * to:
 * /Users/spongebob/serverless-stack/node_modules/.bin/cdk
 */ function getCdkBinPath ( ) { const pkg = "aws-cdk" ; const filePath = require . resolve ( `${ pkg }/package.json` ) ; const matches = filePath . match ( /(^.*[/\\]node_modules)[/\\].*$/ ) ; if ( matches === null || ! matches [ 1 ] ) { throw new Error ( `There was a problem finding ${ pkg }` ) ; } return path . join ( matches [ 1 ] , "aws-cdk" , "bin" , "cdk" ) ; } function buildCDKSpawnEnv ( cdkOptions ) { return { ... process . env , CDK_DISABLE_VERSION_CHECK : true , FORCE_COLOR : cdkOptions . noColor ? 0 : 3 , } ; } async function parseManifest ( cdkOptions ) { const defaultRegion = "us-east-1" ; const stacks = [ ] ; try { const manifestPath = path . join ( cdkOptions . output , "manifest.json" ) ; const manifest = await fs . readJson ( manifestPath ) ; Object . keys ( manifest . artifacts ) . filter ( ( key ) => manifest . artifacts [ key ] . type === "aws:cloudformation:stack" ) . forEach ( ( key ) => { const region = manifest . artifacts [ key ] . environment . split ( "/" ) . pop ( ) ; stacks . push ( { id : key , name : key , region : ! region || region === "unknown-region" ? defaultRegion : region , dependencies : manifest . artifacts [ key ] . dependencies || [ ] , } ) ; } ) ; } catch ( e ) { logger . error ( "Failed to parse generated manifest.json" , e ) ; } return { stacks } ; } async function getLocalTemplate ( cdkOptions , stackName ) { const fileName = `${ stackName }.template.json` ; const filePath = path . join ( cdkOptions . output , fileName ) ; const fileContent = await fs . readFile ( filePath ) ; return fileContent . toString ( ) ; } async function describeStackWithRetry ( { stackName , region } ) { let stackRet ; try { const cfn = new aws . CloudFormation ( { region } ) ; stackRet = await cfn . describeStacks ( { StackName : stackName } ) . promise ( ) ; } catch ( e ) { if ( isRetryableException ( e ) ) { await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ; return await describeStackWithRetry ( { region , stackName } ) ; } throw e ; } return stackRet ; } async function getStackTemplateWithRetry ( { region , stackName } ) { let ret ; try { const cfn = new aws . CloudFormation ( { region } ) ; ret = await cfn . getTemplate ( { StackName : stackName , TemplateStage : "Original" , } ) . promise ( ) ; } catch ( e ) { if ( isRetryableException ( e ) ) { await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ; return await getStackTemplateWithRetry ( { region , stackName } ) ; } throw e ; } return ret ; } function colorFromStackEventStatus ( status ) { if ( ! status ) { return chalk . reset ; } if ( status . indexOf ( "FAILED" ) !== - 1 ) { return chalk . red ; } if ( status . indexOf ( "ROLLBACK" ) !== - 1 ) { return chalk . yellow ; } if ( status . indexOf ( "COMPLETE" ) !== - 1 ) { return chalk . green ; } return chalk . reset ; } function printStackEvent ( stackName , event ) { const statusColor = colorFromStackEventStatus ( event . ResourceStatus ) ; logger . info ( `${ stackName }` + ` | ${ statusColor ( event . ResourceStatus || "" ) }` + ` | ${ event . ResourceType }` + ` | ${ statusColor ( chalk . bold ( event . LogicalResourceId || "" ) ) }` + ( event . ResourceStatusReason ? ` | ${ statusColor ( event . ResourceStatusReason || "" ) }` : "" ) ) ; } function getErrorMessageFromEvents ( events ) { let errorMessage ; const latestResourceStatusReasonByLogicalId = { } ; events . some ( ( { resourceStatus , resourceStatusReason , logicalResourceId } ) => { if ( resourceStatusReason ) { latestResourceStatusReasonByLogicalId [ logicalResourceId ] = resourceStatusReason ; } if ( resourceStatus && ( resourceStatus . endsWith ( "FAILED" ) || resourceStatus . endsWith ( "ROLLBACK_IN_PROGRESS" ) ) ) { errorMessage = latestResourceStatusReasonByLogicalId [ logicalResourceId ] ; return true ; } return false ; } ) ; return errorMessage ; } function isRetryableException ( e ) { return ( ( e . code === "ThrottlingException" && e . message === "Rate exceeded" ) || ( e . code === "Throttling" && e . message === "Rate exceeded" ) || ( e . code === "TooManyRequestsException" && e . message === "Too Many Requests" ) || e . code === "OperationAbortedException" || e . code === "TimeoutError" || e . code === "NetworkingError" ) ; } function isStackNotExistException ( e ) { return ( e . code === "ValidationError" && e . message . startsWith ( "Stack " ) && e . message . endsWith ( " does not exist" ) ) ; } export * as Util from "./util" ; export * from "./update" ; export * from "./packager" ; export * from "./state" ; export * from "./runtime" ; export * from "./bridge" ; export * from "./stacks" ; export * from "./cli" ; export * from "./local" ; export * from "./telemetry" ; export const logger = rootLogger ; export { diff , synth , deployInit , deployPoll , destroyInit , destroyPoll , getCdkVersion , getChildLogger , initializeLogger , STACK_DEPLOY_STATUS , STACK_DESTROY_STATUS , } ;