Skip to content

Commit

Permalink
Handle the case where CloudFormation stack may be already deleted. Al…
Browse files Browse the repository at this point in the history
…so move up DescribeStack operation for checking for DDB tables up sooner so we can bail sooner if necessary. Relates to architect/architect#1150
  • Loading branch information
filmaj committed Jun 4, 2021
1 parent 6eecf30 commit 7df6968
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 32 deletions.
83 changes: 52 additions & 31 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ let ssm = require('./_ssm')
let deleteLogs = require('./_delete-logs')
let { updater, toLogicalID } = require('@architect/utils')

function stackNotFound (StackName, err) {
if (err && err.code == 'ValidationError' && err.message == `Stack with id ${StackName} does not exist`) {
return true
}
return false
}
/**
* @param {object} params - named parameters
* @param {string} params.env - name of environment/stage to delete
Expand Down Expand Up @@ -40,6 +46,7 @@ module.exports = function destroy (params, callback) {
})
}

let stackExists
// actual code
let region = process.env.AWS_REGION
let cloudformation = new aws.CloudFormation({ region })
Expand All @@ -66,15 +73,47 @@ module.exports = function destroy (params, callback) {
StackName
},
function (err, data) {
if (err) callback(err)
else {
let bucket = o => o.OutputKey === 'BucketURL'
let hasBucket = data.Stacks[0].Outputs.find(bucket)
callback(null, hasBucket)
if (stackNotFound(StackName, err)) {
stackExists = false
callback(null, false)
}
else if (err) callback(err)
else callback(null, data.Stacks[0])
})
},

// check for dynamodb tables and if force flag not provided, error out
function (stack, callback) {
if (stack) {
stackExists = true
cloudformation.describeStackResources({
StackName
},
function (err, data) {
if (err) callback(err)
else {
let type = t => t.ResourceType
let table = i => i === 'AWS::DynamoDB::Table'
let hasTables = data.StackResources.map(type).some(table)

if (hasTables && !force) callback(Error('table_exists'))
else callback(null, stack)
}
})
}
else callback(null, stack)
},

// check if static bucket exists in stack
function (stack, callback) {
if (stack) {
let bucket = o => o.OutputKey === 'BucketURL'
let hasBucket = stack.Outputs.find(bucket)
callback(null, hasBucket)
}
else callback(null, false)
},

// delete static assets
function (bucketExists, callback) {
if (bucketExists && force) {
Expand Down Expand Up @@ -120,50 +159,32 @@ module.exports = function destroy (params, callback) {
deleteLogs({ StackName, update }, callback)
},

// check for dynamodb tables
// finally, destroy the cloudformation stack
function (callback) {
cloudformation.describeStackResources({
StackName
},
function (err, data) {
if (err) callback(err)
else {
let type = t => t.ResourceType
let table = i => i === 'AWS::DynamoDB::Table'
let hasTables = data.StackResources.map(type).some(table)
callback(null, hasTables)
}
})
},

function (hasTables, callback) {
if (hasTables && !force) {
callback(Error('table_exists'))
}
else {
// got this far, delete everything
if (stackExists) {
update.start(`Destroying CloudFormation Stack ${StackName}...`)
cloudformation.deleteStack({
StackName,
},
function (err) {
if (err) callback(err)
else callback()
else callback(null, true)
})
}
else callback(null, false)
},

// poll for progress
function (callback) {
// poll for destroy progress in case we are in the process of destroying a stack
function (destroyInProgress, callback) {
if (!destroyInProgress) return callback()
let tries = 1
let max = 6
function checkit () {
cloudformation.describeStacks({
StackName
},
function done (err) {
let msg = `Stack with id ${StackName} does not exist` // Specific AWS message
if (err && err.code == 'ValidationError' && err.message == msg) {
if (stackNotFound(StackName, err)) {
update.done(`Successfully destroyed ${StackName}`)
callback()
}
Expand Down
28 changes: 27 additions & 1 deletion test/unit/index-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
let test = require('tape')
let mocks = require('./mocks')
let aws = require('aws-sdk-mock')
let utils = require('@architect/utils')
let destroy = require('../../src')

let now = true
Expand All @@ -15,6 +16,8 @@ let base = {
}
}

let StackName = utils.toLogicalID(`${base.appname}-${base.env}`)

test('destroy should throw if base parameters are not provided', t => {
t.plan(2)
t.throws(() => {
Expand All @@ -25,7 +28,7 @@ test('destroy should throw if base parameters are not provided', t => {
}, { message: 'Missing params.appname' }, 'missing appname error thrown')
})

test('destroy should error if describeStacks errors', t => {
test('destroy should error if describeStacks errors generically', t => {
t.plan(1)
aws.mock('CloudFormation', 'describeStacks', (ps, cb) => {
cb(true)
Expand All @@ -36,9 +39,32 @@ test('destroy should error if describeStacks errors', t => {
})
})

test('destroy should handle a non-existent Stack gracefully', t => {
t.plan(2)
aws.mock('CloudFormation', 'describeStacks', (ps, cb) => {
cb({ code: 'ValidationError', message: `Stack with id ${StackName} does not exist` })
})
let deleteFlag = false
aws.mock('CloudFormation', 'deleteStack', (params, cb) => {
deleteFlag = true
cb(null)
})
mocks.staticBucket(false) // no static bucket
mocks.deployBucket(false) // no deploy bucket
mocks.dbTables([]) // no tables
mocks.ssmParams([]) // no params
mocks.cloudwatchLogs([]) // no logs
destroy(base, (err) => {
t.notOk(err, 'no error raised')
t.notOk(deleteFlag, 'CloudFormation.deleteStack was not called')
aws.restore()
})
})

test('destroy should error if static bucket exists and force is not provided', t => {
t.plan(1)
mocks.staticBucket('somebucketurl')
mocks.dbTables([]) // no tables
destroy(base, (err) => {
t.equals(err.message, 'bucket_exists', 'bucket_exists error surfaced')
aws.restore()
Expand Down

0 comments on commit 7df6968

Please sign in to comment.