diff --git a/changelog.md b/changelog.md index e233a62..795c752 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,13 @@ -# Architect Sandbox changelog +# Architect Destroy changelog + +--- + +## [1.2.4] 2021-06-09 + +### Changed + +- If the CloudFormation DeleteStack operation detects that the Stack has a status of `DELETE_FAILED`, it will now report this, along with the status reason, and exit with a non-zero code rather than wait for the `destroy` command timeout to run out; this fixes [#1156](https://github.com/architect/architect/issues/1156) +- `destroy` will now exit with a non-zero code if any errors are raised during its execution --- diff --git a/src/index.js b/src/index.js index 2d3fa65..5e86fbd 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,7 @@ module.exports = function destroy (params, callback) { throw ReferenceError('Missing params.appname') } - // StackName → AWS, stackname → user-specified + // StackName → AWS, stackname → user-specified suffix (via --name param) let StackName = toLogicalID(`${appname}-${env}`) if (stackname) { StackName += toLogicalID(stackname) @@ -183,22 +183,26 @@ module.exports = function destroy (params, callback) { cloudformation.describeStacks({ StackName }, - function done (err) { + function done (err, result) { if (stackNotFound(StackName, err)) { update.done(`Successfully destroyed ${StackName}`) - callback() + return callback() } - else { - setTimeout(function delay () { - if (tries === max) { - callback(Error('Destroy failed; hit max retries')) - } - else { - tries += 1 - checkit() - } - }, 10000 * tries) + if (!err && result.Stacks) { + let stack = result.Stacks.find(s => s.StackName === StackName) + if (stack && stack.StackStatus === 'DELETE_FAILED') { + return callback(Error(`CloudFormation Stack "${StackName}" destroy failed: ${stack.StackStatusReason}`)) + } } + setTimeout(function delay () { + if (tries === max) { + callback(Error('Destroy failed; hit max retries')) + } + else { + tries += 1 + checkit() + } + }, 10000 * tries) }) } checkit() diff --git a/test/unit/index-test.js b/test/unit/index-test.js index a6b8045..86195fa 100644 --- a/test/unit/index-test.js +++ b/test/unit/index-test.js @@ -155,7 +155,7 @@ test('destroy should error if DynamoDB tables exist and force is not provided', }) }) -test('destroy should invoke deleteStack', t => { +test('destroy should invoke deleteStack and return once describeStacks return a not found message', t => { t.plan(1) mocks.staticBucket(false) // no static bucket mocks.deployBucket(false) // no deploy bucket @@ -172,3 +172,25 @@ test('destroy should invoke deleteStack', t => { aws.restore() }) }) + +test('destroy should invoke deleteStack and error if describeStacks returns a status of DELETE_FAILED', t => { + t.plan(2) + aws.mock('CloudFormation', 'describeStacks', (ps, cb) => { + cb(null, { Stacks: [ { + StackName: 'PentagonSecurityStaging', + StackStatus: 'DELETE_FAILED', + StackStatusReason: 'task failed successfully', + Outputs: [] + } ] }) + }) + mocks.deployBucket(false) // no deploy bucket + mocks.dbTables([]) // one table + mocks.ssmParams([]) // no params + mocks.cloudwatchLogs([]) // no logs + mocks.deleteStack() + destroy(base, (err) => { + t.ok(err, 'Error returned') + t.match(err.message, /task failed successfully/, 'Delete failed reason provided') + aws.restore() + }) +})