UNPKG

serverless-step-functions

Version:

The module is AWS Step Functions plugin for Serverless Framework

277 lines (245 loc) 9.14 kB
'use strict'; const _ = require('lodash'); const aslValidator = require('asl-validator'); const BbPromise = require('bluebird'); const crypto = require('crypto'); const schema = require('./compileStateMachines.schema'); const { isIntrinsic, translateLocalFunctionNames, convertToFunctionVersion } = require('../../utils/aws'); const logger = require('../../utils/logger'); function generateSubVariableName(element) { return crypto .createHash('md5') .update(JSON.stringify(element)) .digest('hex'); } function toTags(obj) { const tags = []; if (!obj) { return tags; } _.forEach(obj, (Value, Key) => { tags.push({ Key, Value: (Value || '').toString() }); }); return tags; } // return an iterable of // [ ParamName, IntrinsicFunction ] // e.g. [ 'mptFnX05Fb', { Ref: 'MyTopic' } ] // this makes it easy to use _.fromPairs to construct an object afterwards function* getIntrinsicFunctions(obj) { // eslint-disable-next-line no-restricted-syntax for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; if (Array.isArray(value)) { // eslint-disable-next-line guard-for-in, no-restricted-syntax for (const idx in value) { const element = value[idx]; if (isIntrinsic(element)) { const paramName = generateSubVariableName(element); value[idx] = `\${${paramName}}`; yield [paramName, element]; } else { const innerFuncs = Array.from(getIntrinsicFunctions(element)); for (const x of innerFuncs) { yield x; } } } } else if (isIntrinsic(value)) { const paramName = generateSubVariableName(value); // eslint-disable-next-line no-param-reassign obj[key] = `\${${paramName}}`; yield [paramName, value, (obj.Type === 'Wait' && key === 'Seconds')]; } else if (typeof value === 'object') { const innerFuncs = Array.from(getIntrinsicFunctions(value)); for (const x of innerFuncs) { yield x; } } } } } // replace any pseudo parameters, e.g. #{AWS::Region} or #{AWS::AccountId} function replacePseudoParameters(obj) { const json = JSON.stringify(obj); const regex = /#{([^}]+)}/g; if (json.search(regex) >= 0) { const newJson = json.replace(regex, '${$1}'); return { replaced: true, definition: JSON.parse(newJson), }; } return { replaced: false, definition: obj, }; } module.exports = { compileStateMachines() { if (this.isStateMachines()) { this.getAllStateMachines().forEach((stateMachineName) => { const stateMachineObj = this.getStateMachine(stateMachineName); const stateMachineLogicalId = this.getStateMachineLogicalId(stateMachineName, stateMachineObj); let DefinitionString; let RoleArn; let DependsOn = []; let LoggingConfiguration; let TracingConfiguration; let Tags; if (stateMachineObj.inheritGlobalTags === false) { Tags = []; } else { Tags = toTags(this.serverless.service.provider.tags); } const { error, value } = schema.validate(stateMachineObj, { allowUnknown: false }); if (error) { const errorMessage = `State machine [${stateMachineName}] is malformed. ` + 'Please check the README for more info. ' + `${error}`; throw new this.serverless.classes.Error(errorMessage); } if (stateMachineObj.definition) { if (this.serverless.service.stepFunctions.validate) { const { isValid, errorsText } = aslValidator(stateMachineObj.definition); if (isValid) { logger.log(`✓ State machine "${stateMachineName}" definition is valid`); } else { const errorMessage = [ `✕ State machine "${stateMachineName}" definition is invalid:`, errorsText(), ].join('\n'); throw new this.serverless.classes.Error(errorMessage); } } if (typeof stateMachineObj.definition === 'string') { DefinitionString = JSON.stringify(stateMachineObj.definition) .replace(/\\n|\\r|\\n\\r/g, ''); } else { const functionMappings = Array.from(getIntrinsicFunctions(stateMachineObj.definition)); const { replaced, definition } = replacePseudoParameters(stateMachineObj.definition); const definitionString = JSON.stringify(definition, undefined, 2); if (!replaced && _.isEmpty(functionMappings)) { DefinitionString = definitionString; } else if (_.isEmpty(functionMappings)) { DefinitionString = { 'Fn::Sub': definitionString, }; } else { const f = translateLocalFunctionNames.bind(this); let processedDefinitionString = definitionString; functionMappings.forEach((functionMapping) => { if (functionMapping[2]) { processedDefinitionString = processedDefinitionString.replace( // eslint-disable-next-line no-useless-escape new RegExp(`\\\"(\\\$\\\{${functionMapping[0]}\\\})\\\"`, 'g'), '$1', ); } }); const params = _.fromPairs(functionMappings.map(([k, v]) => [k, f(v)])); DefinitionString = { 'Fn::Sub': [ processedDefinitionString, params, ], }; } } } if (stateMachineObj.useExactVersion === true && DefinitionString['Fn::Sub']) { const params = DefinitionString['Fn::Sub'][1]; const f = convertToFunctionVersion.bind(this); const converted = _.mapValues(params, f); DefinitionString['Fn::Sub'][1] = converted; } if (stateMachineObj.role) { RoleArn = stateMachineObj.role; } else { const roleLogicalId = `${stateMachineLogicalId}Role`; RoleArn = { 'Fn::GetAtt': [ roleLogicalId, 'Arn', ], }; DependsOn.push(roleLogicalId); } if (stateMachineObj.dependsOn) { const dependsOn = stateMachineObj.dependsOn; if (_.isArray(dependsOn)) { DependsOn = _.concat(DependsOn, dependsOn); } else { DependsOn.push(dependsOn); } } if (stateMachineObj.tags) { const stateMachineTags = toTags(stateMachineObj.tags); _.forEach(stateMachineTags, tag => Tags.push(tag)); } if (value.loggingConfig) { const Destinations = (value.loggingConfig.destinations || []) .map(arn => ({ CloudWatchLogsLogGroup: { LogGroupArn: arn, }, })); LoggingConfiguration = { Level: value.loggingConfig.level, IncludeExecutionData: value.loggingConfig.includeExecutionData, Destinations, }; } if (value.tracingConfig) { TracingConfiguration = { Enabled: value.tracingConfig.enabled, }; } const stateMachineOutputLogicalId = this .getStateMachineOutputLogicalId(stateMachineName, stateMachineObj); const stateMachineTemplate = { Type: 'AWS::StepFunctions::StateMachine', Properties: { DefinitionString, RoleArn, StateMachineType: stateMachineObj.type, LoggingConfiguration, TracingConfiguration, }, DependsOn, }; if (Tags.length > 0) { stateMachineTemplate.Properties.Tags = Tags; } const newStateMachineObject = { [stateMachineLogicalId]: stateMachineTemplate, }; if (stateMachineObj.retain) { newStateMachineObject[stateMachineLogicalId].DeletionPolicy = 'Retain'; } if (stateMachineObj.name) { newStateMachineObject[ stateMachineLogicalId].Properties.StateMachineName = stateMachineObj.name; } _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, newStateMachineObject); const stateMachineOutPutObject = { Description: 'Current StateMachine Arn', Value: { Ref: stateMachineLogicalId, }, }; const newStateMachineOutPutObject = { [stateMachineOutputLogicalId]: stateMachineOutPutObject, }; if (this.serverless.service.stepFunctions.noOutput !== true) { _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs, newStateMachineOutPutObject); } return BbPromise.resolve(); }); } }, };