From b2e14403a34cbe5d3f3ff4a16409a9dff3dcb672 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 23 May 2018 22:13:05 +0200 Subject: [PATCH] Add better logging --- index.js | 2 ++ lib/cfn.js | 34 ++++++++++++++++++---- lib/logger.js | 19 ++++++++++++ tests/unit/cfn-test.js | 66 ++++++++++++++++++++++++++++++++---------- 4 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 lib/logger.js diff --git a/index.js b/index.js index 8de7bf6..28cf0f7 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ //const RSVP = require('rsvp'); const DeployPluginBase = require('ember-cli-deploy-plugin'); const CfnClient = require('./lib/cfn'); +const Logger = require('./lib/logger'); module.exports = { name: 'ember-cli-deploy-cloudformation', @@ -35,6 +36,7 @@ module.exports = { .reduce((result, item) => Object.assign(result, item), {}); this.cfnClient = this.readConfig('cfnClient') || new CfnClient(options); + this.cfnClient.logger = new Logger(this.log.bind(this)); return this.cfnClient.validateTemplate() .catch(this._errorMessage.bind(this)); diff --git a/lib/cfn.js b/lib/cfn.js index 56fac19..0cf5359 100644 --- a/lib/cfn.js +++ b/lib/cfn.js @@ -23,13 +23,14 @@ class CfnClient { constructor(options) { let awsOptions = { + apiVersion: '2010-05-15', region: options.region, accessKeyId: options.accessKeyId, secretAccessKey: options.secretAccessKey }; if (options.profile) { - awsOptions.credentials = new AWS.SharedIniFileCredentials({ profile }); + awsOptions.credentials = new AWS.SharedIniFileCredentials({ profile: options.profile }); } let cfnOptions = Object.assign({}, options); @@ -68,21 +69,27 @@ class CfnClient { } createStack() { + this.log(`Creating new CloudFormation stack '${this.options.stackName}'...`, 'debug'); return this.awsClient .createStack(this.awsOptions) .promise() - .then(() => this.awsClient.waitFor('stackCreateComplete', { StackName: this.options.stackName }).promise()); + .then(() => this.awsClient.waitFor('stackCreateComplete', { StackName: this.options.stackName }).promise()) + .then(() => this.log(`New CloudFormation stack '${this.options.stackName}' has been created!`)); } updateStack() { + this.log(`Updating CloudFormation stack '${this.options.stackName}'...`, 'debug'); return this.awsClient .updateStack(this.awsOptions) .promise() .then(() => this.awsClient.waitFor('stackUpdateComplete', { StackName: this.options.stackName }).promise()) + .then(() => this.log(`CloudFormation stack '${this.options.stackName}' has been updated!`)) .catch(err => { - if (!String(err).includes('No updates are to be performed')) { - throw err; + if (String(err).includes('No updates are to be performed')) { + this.log(`No updates are to be performed to CloudFormation stack '${this.options.stackName}'`, 'debug'); + return; } + throw err; }); } @@ -100,9 +107,15 @@ class CfnClient { return this.awsClient .describeStacks({ StackName: this.options.stackName }) .promise() - .then((data) => { + .then((result) => { + if (!result.Stacks || !result.Stacks[0]) { + throw new Error('No stack data found from `describeStacks` call'); + } + + let data = result.Stacks[0]; + if (!data.Outputs) { - throw new Error('No Outputs found in `describeStacks` data'); + return {}; } return data.Outputs @@ -141,6 +154,15 @@ class CfnClient { .reduce((result, item) => Object.assign(result, item), {}); } + log(message, type = 'log') { + if (this.logger) { + if (typeof this.logger[type] !== 'function') { + throw new Error(`Logger does not implement ${type} type`); + } + this.logger[type](message); + } + } + } module.exports = CfnClient; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..465768c --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,19 @@ +class MappingLogger { + constructor(logFn) { + this._log = logFn; + } + + log(msg) { + this._log(msg); + } + + error(msg) { + this._log(msg, { color: 'red' }); + } + + debug(msg) { + this._log(msg, { verbose: true }); + } +} + +module.exports = MappingLogger; diff --git a/tests/unit/cfn-test.js b/tests/unit/cfn-test.js index 407198c..da2875f 100644 --- a/tests/unit/cfn-test.js +++ b/tests/unit/cfn-test.js @@ -81,22 +81,25 @@ const expectedOptions = { }; const describeData = { - StackName: 'myStack', - StackStatus: 'CREATE_COMPLETE', - Outputs: [ - { - OutputKey: 'AssetsBucket', - OutputValue: 'abc-123456789' - }, - { - OutputKey: 'CloudFrontDistribution', - OutputValue: 'EFG123456789' - } - ] + Stacks: [{ + StackName: 'myStack', + StackStatus: 'CREATE_COMPLETE', + Outputs: [ + { + OutputKey: 'AssetsBucket', + OutputValue: 'abc-123456789' + }, + { + OutputKey: 'CloudFrontDistribution', + OutputValue: 'EFG123456789' + } + ] + }] }; describe('Cloudformation client', function() { let client; + let logger; beforeEach(function() { client = new CfnClient(options); @@ -115,6 +118,15 @@ describe('Cloudformation client', function() { sinon.stub(client.awsClient, 'validateTemplate').returns({ promise: sinon.fake.resolves() }); + + // minimal logger interface + logger = { + log: sinon.fake(), + error: sinon.fake(), + debug: sinon.fake() + }; + + client.logger = logger; }); afterEach(function() { @@ -134,6 +146,7 @@ describe('Cloudformation client', function() { expect(constructor).to.always.have.been.calledWithNew; expect(constructor).to.have.been.calledWith({ + apiVersion: '2010-05-15', accessKeyId: 'abc', secretAccessKey: 'def', region: 'us-east-1' @@ -148,7 +161,12 @@ describe('Cloudformation client', function() { it('it waits for stackCreateComplete', function() { return expect(callFn()).to.be.fulfilled - .then(() => expect(client.awsClient.waitFor).to.have.been.calledWith('stackCreateComplete', { StackName: 'myStack' })); + .then(() => { + expect(client.awsClient.waitFor).to.have.been.calledWith('stackCreateComplete', { StackName: 'myStack' }); + expect(logger.debug).to.have.been.calledWith(`Creating new CloudFormation stack 'myStack'...`); + expect(logger.log).to.have.been.calledWith(`New CloudFormation stack 'myStack' has been created!`); + expect(logger.debug).to.have.been.calledBefore(logger.log); + }); }); it('rejects when createStack fails', function() { @@ -168,7 +186,12 @@ describe('Cloudformation client', function() { it('it waits for stackUpdateComplete', function() { return expect(callFn()).to.be.fulfilled - .then(() => expect(client.awsClient.waitFor).to.have.been.calledWith('stackUpdateComplete', { StackName: 'myStack' })); + .then(() => { + expect(client.awsClient.waitFor).to.have.been.calledWith('stackUpdateComplete', { StackName: 'myStack' }); + expect(logger.debug).to.have.been.calledWith(`Updating CloudFormation stack 'myStack'...`); + expect(logger.log).to.have.been.calledWith(`CloudFormation stack 'myStack' has been updated!`); + expect(logger.debug).to.have.been.calledBefore(logger.log); + }); }); it('rejects when updateStack fails', function() { @@ -185,7 +208,10 @@ describe('Cloudformation client', function() { }); return expect(callFn()).to.be.fulfilled - .then(() => expect(client.awsClient.waitFor).to.not.have.been.called); + .then(() => { + expect(client.awsClient.waitFor).to.not.have.been.called; + expect(logger.debug).to.have.been.calledWith(`No updates are to be performed to CloudFormation stack 'myStack'`); + }); }); } @@ -258,6 +284,16 @@ describe('Cloudformation client', function() { CloudFrontDistribution: 'EFG123456789' }); }); + + it('returns empty hash when no outputs are found', function() { + let emptyDescribeData = JSON.parse(JSON.stringify(describeData)); + delete emptyDescribeData.Stacks[0].Outputs; + client.awsClient.describeStacks.returns({ + promise: sinon.fake.resolves(emptyDescribeData) + }); + + return expect(client.fetchOutputs()).to.eventually.deep.equal({}); + }); }); });