cfn-stack-event-stream
Advanced tools
Comparing version 0.0.7 to 1.0.0
168
index.js
@@ -1,97 +0,111 @@ | ||
var Readable = require('stream').Readable; | ||
'use strict'; | ||
const Readable = require('stream').Readable; | ||
module.exports = function(cfn, stackName, options) { | ||
options = options || {}; | ||
options = options || {}; | ||
var stream = new Readable({objectMode: true}), | ||
pollInterval = options.pollInterval || 1000, | ||
describing = false, | ||
complete = false, | ||
stackId = stackName, | ||
seen = {}, | ||
events = [], | ||
push = stream.push.bind(stream); | ||
const stream = new Readable({ objectMode: true }), | ||
pollInterval = options.pollInterval || 10000, | ||
seen = {}, | ||
push = stream.push.bind(stream); | ||
if (options.lastEventId) { | ||
seen[options.lastEventId] = true; | ||
} | ||
let describing = false, | ||
complete = false, | ||
stackId = stackName, | ||
events = []; | ||
stream._read = function() { | ||
if (describing || complete) return; | ||
describeStack(); | ||
}; | ||
function describeEvents(nextToken) { | ||
describing = true; | ||
// Describe stacks using stackId (ARN) as CF stacks are actually | ||
// not unique by name. | ||
cfn.describeStackEvents({StackName: stackId, NextToken: nextToken}, function(err, data) { | ||
describing = false; | ||
if (options.lastEventId) { | ||
seen[options.lastEventId] = true; | ||
} | ||
if (err) return stream.emit('error', err); | ||
stream._read = function() { | ||
if (describing || complete) return; | ||
describeStack(); | ||
}; | ||
for (var i = 0; i < (data.StackEvents || []).length; i++) { | ||
var event = data.StackEvents[i]; | ||
function describeEvents(nextToken) { | ||
describing = true; | ||
// Describe stacks using stackId (ARN) as CF stacks are actually | ||
// not unique by name. | ||
cfn.describeStackEvents({ StackName: stackId, NextToken: nextToken }, (err, data) => { | ||
describing = false; | ||
// Assuming StackEvents are in strictly reverse chronological order. | ||
// If we get to what we've seen already we don't need to go on to the | ||
// next page. | ||
if (seen[event.EventId]) | ||
break; | ||
if (err) return stream.emit('error', err); | ||
events.push(event); | ||
seen[event.EventId] = true; | ||
let i; | ||
for (i = 0; i < (data.StackEvents || []).length; i++) { | ||
const event = data.StackEvents[i]; | ||
// If we reach a user initiated event assume this event is the | ||
// initiating event the caller intends to monitor. | ||
if (event.LogicalResourceId === stackName && | ||
event.ResourceType === 'AWS::CloudFormation::Stack' && | ||
event.ResourceStatusReason === 'User Initiated') { | ||
break; | ||
} | ||
} | ||
// Assuming StackEvents are in strictly reverse chronological order, | ||
// stop reading events once we reach one we've seen already. | ||
if (seen[event.EventId]) | ||
break; | ||
if (i === (data.StackEvents || []).length && data.NextToken) { | ||
describeEvents(data.NextToken); | ||
// Collect new events in an array and mark them as "seen". | ||
events.push(event); | ||
seen[event.EventId] = true; | ||
} else if (complete) { | ||
events.reverse().forEach(push); | ||
push(null); | ||
// If we reach a user initiated event assume this event is the | ||
// initiating event the caller intends to monitor. | ||
if (event.LogicalResourceId === stackName && | ||
event.ResourceType === 'AWS::CloudFormation::Stack' && | ||
event.ResourceStatusReason === 'User Initiated') { | ||
break; | ||
} | ||
} | ||
} else { | ||
describeStack(); | ||
events.reverse().forEach(push); | ||
events = []; | ||
} | ||
}).on('retry', function(res) { | ||
// Force a minimum 5s retry delay. | ||
res.error.retryDelay = Math.max(5000, res.error.retryDelay||5000); | ||
stream.emit('retry', res.error); | ||
}); | ||
} | ||
// If we did not find an event on this page we had already seen, paginate. | ||
if (i === (data.StackEvents || []).length && data.NextToken) { | ||
describeEvents(data.NextToken); | ||
} | ||
function describeStack() { | ||
describing = true; | ||
cfn.describeStacks({StackName: stackId}, function(err, data) { | ||
describing = false; | ||
// We know that the update is complete, whatever we have in the events | ||
// array represents the last few events to stream. | ||
else if (complete) { | ||
events.reverse().forEach(push); | ||
push(null); | ||
if (err) return stream.emit('error', err); | ||
if (!data.Stacks.length) return stream.emit('error', new Error('Could not describe stack: ' + stackName)); | ||
} | ||
stackId = data.Stacks[0].StackId; | ||
// The update is not complete, and there aren't any new events or more | ||
// pages to scan. DescribeStack in order to check again to see if the | ||
// update has completed. | ||
else { | ||
setTimeout(describeStack, pollInterval); | ||
events.reverse().forEach(push); | ||
events = []; | ||
} | ||
}).on('retry', (res) => { | ||
// Force a minimum 5s retry delay. | ||
res.error.retryDelay = Math.max(5000, res.error.retryDelay || 5000); | ||
stream.emit('retry', res.error); | ||
}); | ||
} | ||
if (/COMPLETE$/.test(data.Stacks[0].StackStatus)) { | ||
complete = true; | ||
describeEvents(); | ||
} else { | ||
setTimeout(describeEvents, pollInterval); | ||
} | ||
}).on('retry', function(res) { | ||
// Force a minimum 5s retry delay. | ||
res.error.retryDelay = Math.max(5000, res.error.retryDelay||5000); | ||
stream.emit('retry', res.error); | ||
}); | ||
} | ||
function describeStack() { | ||
describing = true; | ||
cfn.describeStacks({ StackName: stackId }, (err, data) => { | ||
describing = false; | ||
return stream; | ||
if (err) return stream.emit('error', err); | ||
if (!data.Stacks.length) return stream.emit('error', new Error('Could not describe stack: ' + stackName)); | ||
stackId = data.Stacks[0].StackId; | ||
if (/COMPLETE$/.test(data.Stacks[0].StackStatus)) { | ||
complete = true; | ||
describeEvents(); | ||
} else { | ||
setTimeout(describeEvents, pollInterval); | ||
} | ||
}).on('retry', (res) => { | ||
// Force a minimum 5s retry delay. | ||
res.error.retryDelay = Math.max(5000, res.error.retryDelay || 5000); | ||
stream.emit('retry', res.error); | ||
}); | ||
} | ||
return stream; | ||
}; |
{ | ||
"name": "cfn-stack-event-stream", | ||
"version": "0.0.7", | ||
"version": "1.0.0", | ||
"description": "A readable stream of CloudFormation stack events", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "tape test/*.test.js" | ||
"lint": "eslint test index.js", | ||
"test": "nyc tape test/*.test.js | tap-spec && npm run lint", | ||
"coverage": "nyc --reporter html tape test/*.test.js && opener coverage/index.html" | ||
}, | ||
@@ -12,7 +14,16 @@ "author": "John Firebaugh <john@mapbox.com>", | ||
"dependencies": { | ||
"aws-sdk": "^2.2.0" | ||
"aws-sdk": "^2.335.0" | ||
}, | ||
"devDependencies": { | ||
"tape": "~4.4.0" | ||
"@mapbox/eslint-config-mapbox": "^1.2.1", | ||
"eslint": "^5.7.0", | ||
"eslint-plugin-node": "^7.0.1", | ||
"nyc": "^13.1.0", | ||
"opener": "^1.5.1", | ||
"tap-spec": "^5.0.0", | ||
"tape": "^4.9.1" | ||
}, | ||
"eslintConfig": { | ||
"extends": "@mapbox/eslint-config-mapbox" | ||
} | ||
} |
@@ -1,153 +0,155 @@ | ||
var test = require('tape'); | ||
var Stream = require('../.'); | ||
var AWS = require('aws-sdk'); | ||
'use strict'; | ||
var cfn = new AWS.CloudFormation({region: 'us-east-1'}); | ||
const test = require('tape'); | ||
const Stream = require('../.'); | ||
const AWS = require('aws-sdk'); | ||
test('emits an error for a non-existent stack', function (assert) { | ||
Stream(cfn, 'cfn-stack-event-stream-test') | ||
.on('data', function (e) {}) | ||
.on('error', function (err) { | ||
assert.ok(err); | ||
assert.end(); | ||
}); | ||
}); | ||
const cfn = new AWS.CloudFormation({ region: 'us-east-1' }); | ||
test('streams events until stack is complete', {timeout: 120000}, function (assert) { | ||
var stackName = 'cfn-stack-event-stream-test-create'; | ||
const template = { | ||
'AWSTemplateFormatVersion': '2010-09-09', | ||
'Description': 'cfn-stack-event-stream-test', | ||
'Parameters': { | ||
'TestParameter': { | ||
'Description': 'A parameter for testing', | ||
'Type': 'String', | ||
'Default': 'TestParameterValue' | ||
} | ||
}, | ||
'Resources': { | ||
'TestTopic': { | ||
'Type': 'AWS::SNS::Topic', | ||
'Properties': { | ||
'DisplayName': { 'Ref': 'TestParameter' } | ||
} | ||
} | ||
} | ||
}; | ||
cfn.createStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(template) | ||
}, function (err) { | ||
if (err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
return; | ||
} | ||
var events = [ | ||
'CREATE_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_COMPLETE AWS::SNS::Topic', | ||
'CREATE_COMPLETE AWS::CloudFormation::Stack' | ||
]; | ||
Stream(cfn, stackName) | ||
.on('data', function (e) { | ||
assert.equal(events[0], e.ResourceStatus + ' ' + e.ResourceType, e.ResourceStatus + ' ' + e.ResourceType); | ||
events.shift(); | ||
}) | ||
.on('end', function () { | ||
updateStack(); | ||
}); | ||
test('emits an error for a non-existent stack', (assert) => { | ||
Stream(cfn, 'cfn-stack-event-stream-test') | ||
.on('data', () => {}) | ||
.on('error', (err) => { | ||
assert.ok(err); | ||
assert.end(); | ||
}); | ||
}); | ||
function updateStack() { | ||
// Modify template for update. | ||
var templateUpdated = JSON.parse(JSON.stringify(template)); | ||
templateUpdated.Resources.NewTopic = { | ||
"Type": "AWS::SNS::Topic", | ||
"Properties" : { | ||
"DisplayName": "Topic2" | ||
} | ||
}; | ||
test('streams events until stack is complete', { timeout: 120000 }, (assert) => { | ||
const stackName = 'cfn-stack-event-stream-test-create'; | ||
cfn.updateStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(templateUpdated) | ||
}, function (err) { | ||
if (err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
return; | ||
} | ||
var events = [ | ||
'UPDATE_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_COMPLETE AWS::SNS::Topic', | ||
'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'UPDATE_COMPLETE AWS::CloudFormation::Stack' | ||
]; | ||
Stream(cfn, stackName) | ||
.on('data', function (e) { | ||
assert.equal(events[0], e.ResourceStatus + ' ' + e.ResourceType, e.ResourceStatus + ' ' + e.ResourceType); | ||
events.shift(); | ||
}) | ||
.on('end', function () { | ||
deleteStack(); | ||
}); | ||
}); | ||
cfn.createStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(template) | ||
}, (err) => { | ||
if (err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
return; | ||
} | ||
const events = [ | ||
'CREATE_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_COMPLETE AWS::SNS::Topic', | ||
'CREATE_COMPLETE AWS::CloudFormation::Stack' | ||
]; | ||
Stream(cfn, stackName) | ||
.on('data', (e) => { | ||
assert.equal(events[0], e.ResourceStatus + ' ' + e.ResourceType, e.ResourceStatus + ' ' + e.ResourceType); | ||
events.shift(); | ||
}) | ||
.on('end', () => { | ||
updateStack(); | ||
}); | ||
}); | ||
function deleteStack() { | ||
cfn.deleteStack({StackName: stackName}, function(err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
function updateStack() { | ||
// Modify template for update. | ||
const templateUpdated = JSON.parse(JSON.stringify(template)); | ||
templateUpdated.Resources.NewTopic = { | ||
'Type': 'AWS::SNS::Topic', | ||
'Properties' : { | ||
'DisplayName': 'Topic2' | ||
} | ||
}; | ||
cfn.updateStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(templateUpdated) | ||
}, (err) => { | ||
if (err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
return; | ||
} | ||
const events = [ | ||
'UPDATE_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_IN_PROGRESS AWS::SNS::Topic', | ||
'CREATE_COMPLETE AWS::SNS::Topic', | ||
'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'UPDATE_COMPLETE AWS::CloudFormation::Stack' | ||
]; | ||
Stream(cfn, stackName) | ||
.on('data', (e) => { | ||
assert.equal(events[0], e.ResourceStatus + ' ' + e.ResourceType, e.ResourceStatus + ' ' + e.ResourceType); | ||
events.shift(); | ||
}) | ||
.on('end', () => { | ||
deleteStack(); | ||
}); | ||
} | ||
}); | ||
} | ||
function deleteStack() { | ||
cfn.deleteStack({ StackName: stackName }, (err) => { | ||
assert.ifError(err); | ||
assert.end(); | ||
}); | ||
} | ||
}); | ||
test('streams events during stack deletion', {timeout: 60000}, function (assert) { | ||
var stackName = 'cfn-stack-event-stream-test-delete', | ||
lastEventId; | ||
test('streams events during stack deletion', { timeout: 60000 }, (assert) => { | ||
const stackName = 'cfn-stack-event-stream-test-delete'; | ||
let lastEventId; | ||
cfn.createStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(template) | ||
}, function (err, stack) { | ||
if (err) { | ||
cfn.createStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(template) | ||
}, (err, stack) => { | ||
if (err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
return; | ||
} | ||
Stream(cfn, stackName) | ||
.on('data', (e) => { | ||
lastEventId = e.EventId; | ||
}) | ||
.on('end', () => { | ||
cfn.deleteStack({ StackName: stackName }, (err) => { | ||
if (err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
return; | ||
} | ||
Stream(cfn, stackName) | ||
.on('data', function (e) { | ||
lastEventId = e.EventId; | ||
} | ||
const events = [ | ||
'DELETE_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'DELETE_IN_PROGRESS AWS::SNS::Topic', | ||
'DELETE_COMPLETE AWS::SNS::Topic', | ||
'DELETE_COMPLETE AWS::CloudFormation::Stack' | ||
]; | ||
Stream(cfn, stack.StackId, { lastEventId: lastEventId }) | ||
.on('data', (e) => { | ||
assert.equal(events[0], e.ResourceStatus + ' ' + e.ResourceType, e.ResourceStatus + ' ' + e.ResourceType); | ||
events.shift(); | ||
}) | ||
.on('end', function () { | ||
cfn.deleteStack({StackName: stackName}, function(err) { | ||
if (err) { | ||
assert.ifError(err); | ||
assert.end(); | ||
return; | ||
} | ||
var events = [ | ||
'DELETE_IN_PROGRESS AWS::CloudFormation::Stack', | ||
'DELETE_IN_PROGRESS AWS::SNS::Topic', | ||
'DELETE_COMPLETE AWS::SNS::Topic', | ||
'DELETE_COMPLETE AWS::CloudFormation::Stack' | ||
]; | ||
Stream(cfn, stack.StackId, {lastEventId: lastEventId}) | ||
.on('data', function (e) { | ||
assert.equal(events[0], e.ResourceStatus + ' ' + e.ResourceType, e.ResourceStatus + ' ' + e.ResourceType); | ||
events.shift(); | ||
}) | ||
.on('end', function () { | ||
assert.end(); | ||
}); | ||
}); | ||
.on('end', () => { | ||
assert.end(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
var template = { | ||
"AWSTemplateFormatVersion": "2010-09-09", | ||
"Description": "cfn-stack-event-stream-test", | ||
"Parameters": { | ||
"TestParameter": { | ||
"Description": "A parameter for testing", | ||
"Type": "String", | ||
"Default": "TestParameterValue" | ||
} | ||
}, | ||
"Resources": { | ||
"TestTopic" : { | ||
"Type": "AWS::SNS::Topic", | ||
"Properties" : { | ||
"DisplayName": { "Ref": "TestParameter" } | ||
} | ||
} | ||
} | ||
}; | ||
@@ -1,61 +0,64 @@ | ||
var test = require('tape'); | ||
var Stream = require('../.'); | ||
var AWS = require('aws-sdk'); | ||
'use strict'; | ||
var cfn = new AWS.CloudFormation({region: 'us-east-1'}); | ||
const test = require('tape'); | ||
const Stream = require('../.'); | ||
const AWS = require('aws-sdk'); | ||
test('handles throttle events', {timeout: 60000}, function (assert) { | ||
var events = [], | ||
managed = [], | ||
stackName = 'cfn-stack-event-stream-test-throttle'; | ||
const cfn = new AWS.CloudFormation({ region: 'us-east-1' }); | ||
cfn.createStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(template) | ||
}, function (err) { | ||
assert.ifError(err); | ||
const template = { | ||
'AWSTemplateFormatVersion': '2010-09-09', | ||
'Description': 'cfn-stack-event-stream-test', | ||
'Resources': { | ||
'Test': { | ||
'Type': 'AWS::AutoScaling::LaunchConfiguration', | ||
'Properties': { | ||
// Hammer CF API to trigger throttling. | ||
var interval = setInterval(function() { | ||
cfn.describeStacks({StackName: stackName}, function(err, data) {}); | ||
}, 100); | ||
} | ||
} | ||
} | ||
}; | ||
Stream(cfn, stackName) | ||
.on('retry', function(err) { | ||
assert.ok(err, 'retry'); | ||
managed.push(err); | ||
clearInterval(interval); | ||
}) | ||
.on('data', function (e) { | ||
assert.ok(e, 'data'); | ||
events.push(e); | ||
}) | ||
.on('end', function () { | ||
cfn.deleteStack({StackName: stackName}, function(err) { | ||
assert.ifError(err); | ||
assert.deepEqual(events.map(function (e) { return e.ResourceStatus; }), [ | ||
'CREATE_IN_PROGRESS', | ||
'CREATE_FAILED', | ||
'ROLLBACK_IN_PROGRESS', | ||
'DELETE_COMPLETE', | ||
'ROLLBACK_COMPLETE' | ||
]); | ||
assert.equal(managed.length > 0, true); | ||
assert.end(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
var template = { | ||
"AWSTemplateFormatVersion": "2010-09-09", | ||
"Description": "cfn-stack-event-stream-test", | ||
"Resources": { | ||
"Test": { | ||
"Type": "AWS::AutoScaling::LaunchConfiguration", | ||
"Properties": { | ||
test('handles throttle events', { timeout: 60000 }, (assert) => { | ||
const events = [], | ||
managed = [], | ||
stackName = 'cfn-stack-event-stream-test-throttle'; | ||
} | ||
} | ||
} | ||
}; | ||
cfn.createStack({ | ||
StackName: stackName, | ||
TemplateBody: JSON.stringify(template) | ||
}, (err) => { | ||
assert.ifError(err); | ||
// Hammer CF API to trigger throttling. | ||
const interval = setInterval(() => { | ||
cfn.describeStacks({ StackName: stackName }, () => {}); | ||
}, 100); | ||
Stream(cfn, stackName) | ||
.on('retry', (err) => { | ||
assert.ok(err, 'retry'); | ||
managed.push(err); | ||
clearInterval(interval); | ||
}) | ||
.on('data', (e) => { | ||
assert.ok(e, 'data'); | ||
events.push(e); | ||
}) | ||
.on('end', () => { | ||
cfn.deleteStack({ StackName: stackName }, (err) => { | ||
assert.ifError(err); | ||
assert.deepEqual(events.map((e) => { return e.ResourceStatus; }), [ | ||
'CREATE_IN_PROGRESS', | ||
'CREATE_FAILED', | ||
'ROLLBACK_IN_PROGRESS', | ||
'DELETE_COMPLETE', | ||
'ROLLBACK_COMPLETE' | ||
]); | ||
assert.equal(managed.length > 0, true); | ||
assert.end(); | ||
}); | ||
}); | ||
}); | ||
}); |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
12735
7
287
1
7
1
Updatedaws-sdk@^2.335.0