serverless
Version:
Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more
468 lines (432 loc) • 14.5 kB
JavaScript
'use strict';
const _ = require('lodash');
const { addCustomResourceToService } = require('../../../../customResources');
const ServerlessError = require('../../../../../../serverless-error');
const { makeAndHashRuleName, makeEventBusTargetId, makeRuleName } = require('./utils');
class AwsCompileEventBridgeEvents {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.provider = this.serverless.getProvider('aws');
this.hooks = {
'initialize': () => {
if (!_.get(this.serverless.service.provider, 'eventBridge.useCloudFormation')) {
const hasFunctionsWithEventBridgeTrigger = Object.values(
this.serverless.service.functions
).some(({ events }) => events.some(({ eventBridge }) => eventBridge));
if (hasFunctionsWithEventBridgeTrigger) {
this.serverless._logDeprecation(
'AWS_EVENT_BRIDGE_CUSTOM_RESOURCE',
'AWS EventBridge resources are not being created using native CloudFormation, this is now possible and the use of custom resources is deprecated. Set `eventBridge.useCloudFormation: true` as a provider property to use this now.'
);
}
}
},
'package:compileEvents': this.compileEventBridgeEvents.bind(this),
};
this.serverless.configSchemaHandler.defineFunctionEvent('aws', 'eventBridge', {
type: 'object',
properties: {
eventBus: {
anyOf: [
{ type: 'string', minLength: 1 },
{ $ref: '#/definitions/awsArnString' },
{ $ref: '#/definitions/awsCfImport' },
{ $ref: '#/definitions/awsCfRef' },
// GetAtt should only reference "Name" property of EventBus
{
type: 'object',
properties: {
'Fn::GetAtt': {
type: 'array',
minItems: 2,
maxItems: 2,
items: [
{ type: 'string', minLength: 1 },
{ type: 'string', enum: ['Name'] },
],
},
},
required: ['Fn::GetAtt'],
additionalProperties: false,
},
],
},
schedule: { pattern: '^(?:cron|rate)\\(.+\\)$' },
pattern: {
type: 'object',
properties: {
'version': {},
'id': {},
'detail-type': {},
'source': {},
'account': {},
'time': {},
'region': {},
'resources': {},
'detail': {},
},
additionalProperties: false,
},
input: { type: 'object' },
inputPath: { type: 'string', minLength: 1, maxLength: 256 },
inputTransformer: {
type: 'object',
properties: {
inputPathsMap: {
type: 'object',
additionalProperties: { type: 'string', minLength: 1 },
},
inputTemplate: { type: 'string', minLength: 1, maxLength: 8192 },
},
required: ['inputTemplate'],
additionalProperties: false,
},
},
anyOf: [{ required: ['pattern'] }, { required: ['schedule'] }],
});
}
compileEventBridgeEvents() {
const { service } = this.serverless;
const { provider } = service;
const { compiledCloudFormationTemplate } = provider;
const iamRoleStatements = [];
const { eventBridge: options } = provider;
const shouldUseCloudFormation = options ? options.useCloudFormation : false;
let hasEventBusesIamRoleStatement = false;
let anyFuncUsesEventBridge = false;
service.getAllFunctions().forEach((functionName) => {
const functionObj = service.getFunction(functionName);
const FunctionName = functionObj.name;
if (functionObj.events) {
functionObj.events.forEach((event, idx) => {
if (event.eventBridge) {
idx++;
anyFuncUsesEventBridge = true;
const EventBus = event.eventBridge.eventBus;
const Schedule = event.eventBridge.schedule;
const Pattern = event.eventBridge.pattern;
const Input = event.eventBridge.input;
const InputPath = event.eventBridge.inputPath;
let InputTransformer = event.eventBridge.inputTransformer;
const RuleName = makeAndHashRuleName({
functionName: FunctionName,
index: idx,
});
if ([Input, InputPath, InputTransformer].filter(Boolean).length > 1) {
throw new ServerlessError(
[
'You can only set one of input, inputPath, or inputTransformer ',
'properties for eventBridge events.',
].join(''),
'EVENTBRIDGE_MULTIPLE_INPUT_PROPERTIES'
);
}
if (InputTransformer) {
InputTransformer = _.mapKeys(
InputTransformer,
(value, key) => key[0].toLocaleUpperCase() + key.slice(1)
);
}
const eventBusName = EventBus;
// Custom resources will be deprecated in next major release
if (!shouldUseCloudFormation) {
const results = this.compileWithCustomResource({
eventBusName,
EventBus,
compiledCloudFormationTemplate,
functionName,
RuleName,
Input,
InputPath,
InputTransformer,
Pattern,
Schedule,
FunctionName,
idx,
hasEventBusesIamRoleStatement,
iamRoleStatements,
});
results.iamRoleStatements.forEach((statement) => iamRoleStatements.push(statement));
hasEventBusesIamRoleStatement = results.hasEventBusesIamRoleStatement;
} else {
this.compileWithCloudFormation({
eventBusName,
EventBus,
compiledCloudFormationTemplate,
functionName,
RuleName,
Input,
InputPath,
InputTransformer,
Pattern,
Schedule,
FunctionName,
idx,
hasEventBusesIamRoleStatement,
iamRoleStatements,
});
}
}
});
}
});
// These permissions are for the custom resource lambda
if (!shouldUseCloudFormation && anyFuncUsesEventBridge) {
return this._addCustomResourceToService({ iamRoleStatements });
}
return null;
}
compileWithCustomResource({
eventBusName,
EventBus,
compiledCloudFormationTemplate,
functionName,
RuleName,
Input,
InputPath,
InputTransformer,
Pattern,
Schedule,
FunctionName,
idx,
hasEventBusesIamRoleStatement,
}) {
if (_.isObject(eventBusName)) {
throw new ServerlessError(
'Referencing event bus with CloudFormation intrinsic functions is not supported for EventBrigde integration backed by Custom Resources. Please use `provider.eventBridge.useCloudFormation` setting to use native CloudFormation support for EventBridge.',
'ERROR_INVALID_REFERENCE_TO_EVENT_BUS_CUSTOM_RESOURCE'
);
}
const iamRoleStatements = [];
if (typeof eventBusName === 'string' && eventBusName.startsWith('arn')) {
eventBusName = EventBus.slice(EventBus.indexOf('/') + 1);
}
const eventFunctionLogicalId = this.provider.naming.getLambdaLogicalId(functionName);
const customResourceFunctionLogicalId = this.provider.naming.getCustomResourceEventBridgeHandlerFunctionLogicalId();
const customEventBridgeResourceLogicalId = this.provider.naming.getCustomResourceEventBridgeResourceLogicalId(
functionName,
idx
);
const customEventBridge = {
Type: 'Custom::EventBridge',
Version: 1.0,
DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId],
Properties: {
ServiceToken: {
'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'],
},
FunctionName,
EventBridgeConfig: {
RuleName,
EventBus,
Schedule,
Pattern,
Input,
InputPath,
InputTransformer,
},
},
};
compiledCloudFormationTemplate.Resources[
customEventBridgeResourceLogicalId
] = customEventBridge;
if (!hasEventBusesIamRoleStatement && eventBusName && eventBusName !== 'default') {
iamRoleStatements.push({
Effect: 'Allow',
Resource: {
'Fn::Join': [
':',
[
'arn',
{ Ref: 'AWS::Partition' },
'events',
{ Ref: 'AWS::Region' },
{ Ref: 'AWS::AccountId' },
'event-bus/*',
],
],
},
Action: ['events:CreateEventBus', 'events:DeleteEventBus'],
});
hasEventBusesIamRoleStatement = true;
}
return {
iamRoleStatements,
hasEventBusesIamRoleStatement,
};
}
compileWithCloudFormation({
eventBusName: _eventBusName,
EventBus,
compiledCloudFormationTemplate,
functionName,
RuleName,
Input,
InputPath,
InputTransformer,
Pattern,
Schedule,
FunctionName,
idx,
}) {
let eventBusResource;
let eventBusExists = false;
let eventBusName = _eventBusName;
// It suggests that the object already exists and is being imported
if (_.isObject(eventBusName)) {
eventBusExists = true;
}
// Does the resource already exist? ARN string - assume it is valid - CF will validate ultimately
if (typeof eventBusName === 'string' && eventBusName.startsWith('arn')) {
eventBusExists = true;
eventBusName = EventBus.slice(EventBus.indexOf('/') + 1);
}
const shouldCreateEventBus = !eventBusExists && eventBusName && eventBusName !== 'default';
if (shouldCreateEventBus) {
// Create EventBus Resource
eventBusResource = {
Type: 'AWS::Events::EventBus',
Properties: {
Name: eventBusName,
},
};
compiledCloudFormationTemplate.Resources[
this.provider.naming.getEventBridgeEventBusLogicalId(eventBusName)
] = eventBusResource;
}
const targetBase = {
Arn: {
'Fn::GetAtt': [this.provider.naming.getLambdaLogicalId(functionName), 'Arn'],
},
Id: makeEventBusTargetId(RuleName),
};
const target = this.addInputConfigToTarget({
target: targetBase,
Input,
InputPath,
InputTransformer,
});
// Create a rule
const eventRuleResource = {
Type: 'AWS::Events::Rule',
Properties: {
// default event bus is used when EventBusName is not set
EventBusName: eventBusName === 'default' ? undefined : eventBusName,
EventPattern: JSON.stringify(Pattern),
Name: RuleName,
ScheduleExpression: Schedule,
State: 'ENABLED',
Targets: [target],
},
};
// If this stack is creating the event bus the rule must depend on it to ensure stack can be removed
if (shouldCreateEventBus) {
eventRuleResource.DependsOn = this.provider.naming.getEventBridgeEventBusLogicalId(
eventBusName
);
}
const ruleNameLogicalIdStub = makeRuleName({
functionName: FunctionName,
index: idx,
});
compiledCloudFormationTemplate.Resources[
this.provider.naming.getEventBridgeRuleLogicalId(ruleNameLogicalIdStub)
] = eventRuleResource;
const ruleNameArnPath = eventBusName ? [eventBusName, RuleName] : [RuleName];
const lambdaPermissionResource = {
Type: 'AWS::Lambda::Permission',
Properties: {
Action: 'lambda:InvokeFunction',
FunctionName: {
Ref: this.provider.naming.getLambdaLogicalId(functionName),
},
Principal: 'events.amazonaws.com',
SourceArn: {
'Fn::Join': [
':',
[
'arn',
{ Ref: 'AWS::Partition' },
'events',
{ Ref: 'AWS::Region' },
{ Ref: 'AWS::AccountId' },
{
'Fn::Join': ['/', ['rule', ...ruleNameArnPath]],
},
],
],
},
},
};
compiledCloudFormationTemplate.Resources[
this.provider.naming.getEventBridgeLambdaPermissionLogicalId(functionName, idx)
] = lambdaPermissionResource;
}
_addCustomResourceToService({ iamRoleStatements: _iamRoleStatements }) {
const iamRoleStatements = _iamRoleStatements;
const ruleResources = {
'Fn::Join': [
':',
[
'arn',
{ Ref: 'AWS::Partition' },
'events',
{ Ref: 'AWS::Region' },
{ Ref: 'AWS::AccountId' },
'rule/*',
],
],
};
iamRoleStatements.push({
Effect: 'Allow',
Resource: ruleResources,
Action: ['events:PutRule', 'events:RemoveTargets', 'events:PutTargets', 'events:DeleteRule'],
});
const functionResources = {
'Fn::Join': [
':',
[
'arn',
{ Ref: 'AWS::Partition' },
'lambda',
{ Ref: 'AWS::Region' },
{ Ref: 'AWS::AccountId' },
'function',
'*',
],
],
};
iamRoleStatements.push({
Effect: 'Allow',
Resource: functionResources,
Action: ['lambda:AddPermission', 'lambda:RemovePermission'],
});
if (iamRoleStatements.length) {
return addCustomResourceToService(this.provider, 'eventBridge', iamRoleStatements);
}
return null;
}
addInputConfigToTarget({ target, Input, InputPath, InputTransformer }) {
if (Input) {
target = Object.assign(target, {
Input: JSON.stringify(Input),
});
return target;
}
if (InputPath) {
target = Object.assign(target, {
InputPath,
});
return target;
}
if (InputTransformer) {
target = Object.assign(target, {
InputTransformer,
});
return target;
}
return target;
}
}
module.exports = AwsCompileEventBridgeEvents;