UNPKG

serverless-step-functions

Version:

The module is AWS Step Functions plugin for Serverless Framework

583 lines (525 loc) 19.5 kB
'use strict'; const _ = require('lodash'); const expect = require('chai').expect; const sinon = require('sinon'); const Serverless = require('serverless/lib/Serverless'); const AwsProvider = require('serverless/lib/plugins/aws/provider'); const ServerlessStepFunctions = require('./../../index'); describe('#compileAlarms', () => { let consoleLogSpy; let serverless; let serverlessStepFunctions; beforeEach(() => { consoleLogSpy = sinon.spy(); serverless = new Serverless(); serverless.servicePath = true; serverless.service.service = 'step-functions'; serverless.configSchemaHandler = { // eslint-disable-next-line no-unused-vars defineTopLevelProperty: (propertyName, propertySchema) => {}, }; serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; serverless.setProvider('aws', new AwsProvider(serverless)); serverless.cli = { consoleLog: consoleLogSpy }; const options = { stage: 'dev', region: 'ap-northeast-1', }; serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); }); const validateCloudWatchAlarm = (alarm) => { expect(alarm.Type).to.equal('AWS::CloudWatch::Alarm'); expect(alarm.Properties.Namespace).to.equal('AWS/States'); expect(alarm.Properties.Threshold).to.equal(1); expect(alarm.Properties.Period).to.equal(60); expect(alarm.Properties.Statistic).to.equal('Sum'); expect(alarm.Properties.Dimensions).to.have.lengthOf(1); expect(alarm.Properties.Dimensions[0].Name).to.equal('StateMachineArn'); }; it('should generate CloudWatch Alarms', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', alarm: '${self:service}-${opt:stage}-alerts-alarm', insufficientData: '${self:service}-${opt:stage}-alerts-missing', }, metrics: [ 'executionsTimedOut', 'executionsFailed', 'executionsAborted', 'executionThrottled', 'executionsSucceeded', ], }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(resources).to.have.property('StateMachineBeta1ExecutionsTimedOutAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionsTimedOutAlarm); expect(resources).to.have.property('StateMachineBeta1ExecutionsFailedAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionsFailedAlarm); expect(resources).to.have.property('StateMachineBeta1ExecutionsAbortedAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionsAbortedAlarm); expect(resources).to.have.property('StateMachineBeta1ExecutionThrottledAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta1ExecutionThrottledAlarm); expect(resources).to.have.property('StateMachineBeta2ExecutionsTimedOutAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionsTimedOutAlarm); expect(resources).to.have.property('StateMachineBeta2ExecutionsFailedAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionsFailedAlarm); expect(resources).to.have.property('StateMachineBeta2ExecutionsAbortedAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionsAbortedAlarm); expect(resources).to.have.property('StateMachineBeta2ExecutionThrottledAlarm'); validateCloudWatchAlarm(resources.StateMachineBeta2ExecutionThrottledAlarm); expect(consoleLogSpy.callCount).equal(0); }); it('should not generate logs when no CloudWatch Alarms are defiened', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(_.keys(resources)).to.have.lengthOf(0); expect(consoleLogSpy.callCount).equal(0); }); it('should not generate CloudWatch Alarms when alarms.topics is missing', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { metrics: [ 'executionsTimedOut', ], }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(_.keys(resources)).to.have.lengthOf(0); expect(consoleLogSpy.callCount).equal(2); }); it('should not generate CloudWatch Alarms when alarms.topics is empty', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: {}, metrics: [ 'executionsTimedOut', ], }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(_.keys(resources)).to.have.lengthOf(0); expect(consoleLogSpy.callCount).equal(2); }); it('should not generate CloudWatch Alarms when alarms.metrics is missing', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', }, }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(_.keys(resources)).to.have.lengthOf(0); expect(consoleLogSpy.callCount).equal(2); }); it('should not generate CloudWatch Alarms for unsupported metrics', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', }, metrics: [ 'executionsFailed', 'executionsFail', ], }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; // valid metrics => CW alarms expect(resources).to.have.property('StateMachineBeta1ExecutionsFailedAlarm'); expect(resources).to.have.property('StateMachineBeta2ExecutionsFailedAlarm'); // but invalid metric names are skipped expect(_.keys(resources)).to.have.lengthOf(2); expect(consoleLogSpy.callCount).equal(2); }); it('should use specified treatMissingData for all alarms', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', alarm: '${self:service}-${opt:stage}-alerts-alarm', insufficientData: '${self:service}-${opt:stage}-alerts-missing', }, metrics: [ 'executionsTimedOut', 'executionsFailed', 'executionsAborted', 'executionThrottled', 'executionsSucceeded', ], treatMissingData: 'ignore', }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; const verify = (resourceName) => { expect(resources).to.have.property(resourceName); expect(resources[resourceName].Properties.TreatMissingData).to.equal('ignore'); }; verify('StateMachineBeta1ExecutionsTimedOutAlarm'); verify('StateMachineBeta1ExecutionsFailedAlarm'); verify('StateMachineBeta1ExecutionsAbortedAlarm'); verify('StateMachineBeta1ExecutionThrottledAlarm'); verify('StateMachineBeta2ExecutionsTimedOutAlarm'); verify('StateMachineBeta2ExecutionsFailedAlarm'); verify('StateMachineBeta2ExecutionsAbortedAlarm'); verify('StateMachineBeta2ExecutionThrottledAlarm'); expect(consoleLogSpy.callCount).equal(0); }); it('should allow individual alarms to override default treatMissingData', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', alarm: '${self:service}-${opt:stage}-alerts-alarm', insufficientData: '${self:service}-${opt:stage}-alerts-missing', }, metrics: [ 'executionsTimedOut', { metric: 'executionsFailed', treatMissingData: 'breaching' }, 'executionsAborted', 'executionThrottled', 'executionsSucceeded', ], treatMissingData: 'ignore', }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: genStateMachine('stateMachineBeta1'), myStateMachine2: genStateMachine('stateMachineBeta2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; const verify = (resourceName, expectedConfig = 'ignore') => { expect(resources).to.have.property(resourceName); expect(resources[resourceName].Properties.TreatMissingData).to.equal(expectedConfig); }; verify('StateMachineBeta1ExecutionsTimedOutAlarm'); verify('StateMachineBeta1ExecutionsFailedAlarm', 'breaching'); verify('StateMachineBeta1ExecutionsAbortedAlarm'); verify('StateMachineBeta1ExecutionThrottledAlarm'); verify('StateMachineBeta2ExecutionsTimedOutAlarm'); verify('StateMachineBeta2ExecutionsFailedAlarm', 'breaching'); verify('StateMachineBeta2ExecutionsAbortedAlarm'); verify('StateMachineBeta2ExecutionThrottledAlarm'); expect(consoleLogSpy.callCount).equal(0); }); it('should allow alarms to override default logical ID', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', alarm: '${self:service}-${opt:stage}-alerts-alarm', insufficientData: '${self:service}-${opt:stage}-alerts-missing', }, metrics: [ { metric: 'executionsFailed', logicalId: 'MyAlarm', treatMissingData: 'breaching' }, ], treatMissingData: 'ignore', }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine: genStateMachine('stateMachineBeta'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(resources).to.have.property('MyAlarm'); expect(consoleLogSpy.callCount).equal(0); }); it('should generate CloudWatch Alarms with nameTemplate', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', alarm: '${self:service}-${opt:stage}-alerts-alarm', insufficientData: '${self:service}-${opt:stage}-alerts-missing', }, nameTemplate: '$[stateMachineName]-$[cloudWatchMetricName]-alarm', metrics: [ 'executionsTimedOut', 'executionsFailed', 'executionsAborted', 'executionThrottled', 'executionsSucceeded', ], }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine: genStateMachine('stateCustomName1'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(resources.StateCustomName1ExecutionsTimedOutAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsTimedOut-alarm'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionsTimedOutAlarm); expect(resources.StateCustomName1ExecutionsFailedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsFailed-alarm'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionsFailedAlarm); expect(resources.StateCustomName1ExecutionsAbortedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsAborted-alarm'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionsAbortedAlarm); expect(resources.StateCustomName1ExecutionThrottledAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionThrottled-alarm'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionThrottledAlarm); expect(consoleLogSpy.callCount).equal(0); }); it('should generate CloudWatch Alarms with invalid nameTemplate', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', alarm: '${self:service}-${opt:stage}-alerts-alarm', insufficientData: '${self:service}-${opt:stage}-alerts-missing', }, nameTemplate: '$[stateMachineName]-$[invalidProp]-alarm', metrics: [ 'executionsTimedOut', 'executionsFailed', 'executionsAborted', 'executionThrottled', 'executionsSucceeded', ], }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine: genStateMachine('stateCustomName2'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(resources.StateCustomName2ExecutionsTimedOutAlarm.Properties).not.have.property('AlarmName'); validateCloudWatchAlarm(resources.StateCustomName2ExecutionsTimedOutAlarm); expect(resources.StateCustomName2ExecutionsFailedAlarm.Properties).not.have.property('AlarmName'); validateCloudWatchAlarm(resources.StateCustomName2ExecutionsFailedAlarm); expect(resources.StateCustomName2ExecutionsAbortedAlarm.Properties).not.have.property('AlarmName'); validateCloudWatchAlarm(resources.StateCustomName2ExecutionsAbortedAlarm); expect(resources.StateCustomName2ExecutionThrottledAlarm.Properties).not.have.property('AlarmName'); validateCloudWatchAlarm(resources.StateCustomName2ExecutionThrottledAlarm); expect(consoleLogSpy.callCount).equal(5); }); it('should generate CloudWatch Alarms with custom alarm name', () => { const genStateMachine = name => ({ name, definition: { StartAt: 'A', States: { A: { Type: 'Pass', End: true, }, }, }, alarms: { topics: { ok: '${self:service}-${opt:stage}-alerts-ok', alarm: '${self:service}-${opt:stage}-alerts-alarm', insufficientData: '${self:service}-${opt:stage}-alerts-missing', }, nameTemplate: '$[stateMachineName]-$[cloudWatchMetricName]-alarm', metrics: [ { metric: 'executionsTimedOut', alarmName: 'mycustom-name', }, 'executionsFailed', 'executionsAborted', 'executionThrottled', 'executionsSucceeded', ], }, }); serverless.service.stepFunctions = { stateMachines: { myStateMachine: genStateMachine('stateCustomName1'), }, }; serverlessStepFunctions.compileAlarms(); const resources = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources; expect(resources.StateCustomName1ExecutionsTimedOutAlarm.Properties.AlarmName).to.be.equal('mycustom-name'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionsTimedOutAlarm); expect(resources.StateCustomName1ExecutionsFailedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsFailed-alarm'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionsFailedAlarm); expect(resources.StateCustomName1ExecutionsAbortedAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionsAborted-alarm'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionsAbortedAlarm); expect(resources.StateCustomName1ExecutionThrottledAlarm.Properties.AlarmName).to.be.equal('stateCustomName1-ExecutionThrottled-alarm'); validateCloudWatchAlarm(resources.StateCustomName1ExecutionThrottledAlarm); expect(consoleLogSpy.callCount).equal(0); }); });