mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
feat(AWS EventBridge): Support for using native CloudFormation (#8437)
Co-authored-by: Piotr Grzesik <pj.grzesik@gmail.com>
This commit is contained in:
parent
9b030ad5f4
commit
13444caa28
@ -17,6 +17,16 @@ disabledDeprecations:
|
||||
- '*' # To disable all deprecation messages
|
||||
```
|
||||
|
||||
<a name="AWS_EVENT_BRIDGE_CUSTOM_RESOURCE"><div> </div></a>
|
||||
|
||||
## AWS EventBridge lambda event triggers
|
||||
|
||||
Deprecation code: `AWS_EVENT_BRIDGE_CUSTOM_RESOURCE`
|
||||
|
||||
Starting with v3.0.0 AWS EventBridge lambda event triggers and all associated EventBridge resources will be deployed using native CloudFormation resources instead of a custom resource that used a lambda to deploy them via the AWS SDK/API.
|
||||
|
||||
Adapt to this behavior now by setting `provider.eventBridge.useCloudFormation: true`.
|
||||
|
||||
<a name="NEW_VARIABLES_RESOLVER"><div> </div></a>
|
||||
|
||||
## New variables resolver
|
||||
|
||||
@ -127,7 +127,7 @@ module.exports = {
|
||||
|
||||
// Lambda
|
||||
getNormalizedFunctionName(functionName) {
|
||||
return this.normalizeName(functionName.replace(/-/g, 'Dash').replace(/_/g, 'Underscore'));
|
||||
return this.getNormalizedResourceName(functionName);
|
||||
},
|
||||
extractLambdaNameFromArn(functionArn) {
|
||||
return functionArn.substring(functionArn.lastIndexOf(':') + 1);
|
||||
@ -567,6 +567,18 @@ module.exports = {
|
||||
getCustomResourceEventBridgeResourceLogicalId(functionName, idx) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}CustomEventBridge${idx}`;
|
||||
},
|
||||
getNormalizedResourceName(resourceName) {
|
||||
return this.normalizeName(resourceName.replace(/-/g, 'Dash').replace(/_/g, 'Underscore'));
|
||||
},
|
||||
getEventBridgeEventBusLogicalId(eventBusName) {
|
||||
return `${this.getNormalizedResourceName(eventBusName)}EventBridgeEventBus`;
|
||||
},
|
||||
getEventBridgeRuleLogicalId(ruleName) {
|
||||
return `${this.normalizeNameToAlphaNumericOnly(ruleName)}EventBridgeRule`;
|
||||
},
|
||||
getEventBridgeLambdaPermissionLogicalId(functionName, idx) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}EventBridgeLambdaPermission${idx}`;
|
||||
},
|
||||
|
||||
// API Gateway Account Logs Write Role
|
||||
getCustomResourceApiGatewayAccountCloudWatchRoleHandlerFunctionName() {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const { addCustomResourceToService } = require('../../../../customResources');
|
||||
const ServerlessError = require('../../../../../../serverless-error');
|
||||
const { makeAndHashRuleName, makeEventBusTargetId, makeRuleName } = require('./utils');
|
||||
|
||||
class AwsCompileEventBridgeEvents {
|
||||
constructor(serverless, options) {
|
||||
@ -12,13 +12,28 @@ class AwsCompileEventBridgeEvents {
|
||||
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: { type: 'string', minLength: 1 },
|
||||
eventBus: {
|
||||
anyOf: [{ type: 'string', minLength: 1 }, { $ref: '#/definitions/awsArn' }],
|
||||
},
|
||||
schedule: { pattern: '^(?:cron|rate)\\(.+\\)$' },
|
||||
pattern: {
|
||||
type: 'object',
|
||||
@ -59,6 +74,8 @@ class AwsCompileEventBridgeEvents {
|
||||
const { provider } = service;
|
||||
const { compiledCloudFormationTemplate } = provider;
|
||||
const iamRoleStatements = [];
|
||||
const { eventBridge: options } = provider;
|
||||
const shouldUseCloudFormation = options ? options.useCloudFormation : false;
|
||||
let hasEventBusesIamRoleStatement = false;
|
||||
let anyFuncUsesEventBridge = false;
|
||||
|
||||
@ -71,30 +88,16 @@ class AwsCompileEventBridgeEvents {
|
||||
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 RuleNameSuffix = `rule-${idx}`;
|
||||
let RuleName = `${FunctionName}-${RuleNameSuffix}`;
|
||||
if (RuleName.length > 64) {
|
||||
// Rule names cannot be longer than 64.
|
||||
// Temporary solution until we have https://github.com/serverless/serverless/issues/6598
|
||||
RuleName = `${RuleName.slice(0, 31 - RuleNameSuffix.length)}${crypto
|
||||
.createHash('md5')
|
||||
.update(RuleName)
|
||||
.digest('hex')}-${RuleNameSuffix}`;
|
||||
}
|
||||
|
||||
const eventFunctionLogicalId = this.provider.naming.getLambdaLogicalId(functionName);
|
||||
const customResourceFunctionLogicalId = this.provider.naming.getCustomResourceEventBridgeHandlerFunctionLogicalId();
|
||||
const customEventBridgeResourceLogicalId = this.provider.naming.getCustomResourceEventBridgeResourceLogicalId(
|
||||
functionName,
|
||||
idx
|
||||
);
|
||||
const RuleName = makeAndHashRuleName({
|
||||
functionName: FunctionName,
|
||||
index: idx,
|
||||
});
|
||||
|
||||
if ([Input, InputPath, InputTransformer].filter(Boolean).length > 1) {
|
||||
throw new ServerlessError(
|
||||
@ -112,115 +115,330 @@ class AwsCompileEventBridgeEvents {
|
||||
);
|
||||
}
|
||||
|
||||
const customEventBridge = {
|
||||
[customEventBridgeResourceLogicalId]: {
|
||||
Type: 'Custom::EventBridge',
|
||||
Version: 1.0,
|
||||
DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId],
|
||||
Properties: {
|
||||
ServiceToken: {
|
||||
'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'],
|
||||
},
|
||||
FunctionName,
|
||||
EventBridgeConfig: {
|
||||
RuleName,
|
||||
EventBus,
|
||||
Schedule,
|
||||
Pattern,
|
||||
Input,
|
||||
InputPath,
|
||||
InputTransformer,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
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,
|
||||
});
|
||||
|
||||
_.merge(compiledCloudFormationTemplate.Resources, customEventBridge);
|
||||
|
||||
if (EventBus) {
|
||||
let eventBusName = EventBus;
|
||||
if (EventBus.startsWith('arn')) {
|
||||
eventBusName = EventBus.slice(EventBus.indexOf('/') + 1);
|
||||
}
|
||||
|
||||
if (!hasEventBusesIamRoleStatement && eventBusName !== 'default') {
|
||||
const eventBusResources = {
|
||||
'Fn::Join': [
|
||||
':',
|
||||
[
|
||||
'arn',
|
||||
{ Ref: 'AWS::Partition' },
|
||||
'events',
|
||||
{ Ref: 'AWS::Region' },
|
||||
{ Ref: 'AWS::AccountId' },
|
||||
'event-bus/*',
|
||||
],
|
||||
],
|
||||
};
|
||||
iamRoleStatements.push({
|
||||
Effect: 'Allow',
|
||||
Resource: eventBusResources,
|
||||
Action: ['events:CreateEventBus', 'events:DeleteEventBus'],
|
||||
});
|
||||
hasEventBusesIamRoleStatement = true;
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (anyFuncUsesEventBridge) {
|
||||
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'],
|
||||
});
|
||||
// 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;
|
||||
|
||||
40
lib/plugins/aws/package/compile/events/eventBridge/utils.js
Normal file
40
lib/plugins/aws/package/compile/events/eventBridge/utils.js
Normal file
@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const makeAndHashRuleName = ({ functionName, index }) => {
|
||||
const name = makeRuleName({ functionName, index });
|
||||
if (name.length > 64) {
|
||||
// Rule names cannot be longer than 64.
|
||||
// Temporary solution until we have https://github.com/serverless/serverless/issues/6598
|
||||
return hashName(name, makeRuleNameSuffix(index));
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
const makeRuleName = ({ functionName, index }) => `${functionName}-${makeRuleNameSuffix(index)}`;
|
||||
|
||||
const makeRuleNameSuffix = (index) => `rule-${index}`;
|
||||
|
||||
const makeEventBusTargetId = (ruleName) => {
|
||||
const suffix = 'target';
|
||||
let targetId = `${ruleName}-${suffix}`;
|
||||
if (targetId.length > 64) {
|
||||
// Target ids cannot be longer than 64.
|
||||
targetId = hashName(targetId, suffix);
|
||||
}
|
||||
return targetId;
|
||||
};
|
||||
|
||||
const hashName = (name, suffix) =>
|
||||
`${name.slice(0, 31 - suffix.length)}${crypto
|
||||
.createHash('md5')
|
||||
.update(name)
|
||||
.digest('hex')}-${suffix}`;
|
||||
|
||||
module.exports = {
|
||||
makeAndHashRuleName,
|
||||
makeRuleName,
|
||||
hashName,
|
||||
makeEventBusTargetId,
|
||||
};
|
||||
@ -730,6 +730,13 @@ class AwsProvider {
|
||||
anyOf: ['REGIONAL', 'EDGE', 'PRIVATE'].map(caseInsensitive),
|
||||
},
|
||||
environment: { $ref: '#/definitions/awsLambdaEnvironment' },
|
||||
eventBridge: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
useCloudFormation: { const: true },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
httpApi: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
7
test/fixtures/eventBridge/core.js
vendored
7
test/fixtures/eventBridge/core.js
vendored
@ -28,4 +28,9 @@ function eventBusArn(event, context, callback) {
|
||||
return callback(null, event);
|
||||
}
|
||||
|
||||
module.exports = { eventBusDefault, eventBusDefaultArn, eventBusCustom, eventBusArn };
|
||||
module.exports = {
|
||||
eventBusDefault,
|
||||
eventBusDefaultArn,
|
||||
eventBusCustom,
|
||||
eventBusArn,
|
||||
};
|
||||
|
||||
@ -14,89 +14,132 @@ const {
|
||||
|
||||
const { deployService, removeService, getMarkers } = require('../utils/integration');
|
||||
|
||||
describe('AWS - Event Bridge Integration Test', function () {
|
||||
this.timeout(1000 * 60 * 10); // Involves time-taking deploys
|
||||
let serviceName;
|
||||
let stackName;
|
||||
let servicePath;
|
||||
let namedEventBusName;
|
||||
let arnEventBusName;
|
||||
let arnEventBusArn;
|
||||
const eventSource = 'serverless.test';
|
||||
const stage = 'dev';
|
||||
const putEventEntries = [
|
||||
{
|
||||
Source: eventSource,
|
||||
DetailType: 'ServerlessDetailType',
|
||||
Detail: '{"Key1":"Value1"}',
|
||||
},
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
const serviceData = await fixtures.setup('eventBridge');
|
||||
({ servicePath } = serviceData);
|
||||
serviceName = serviceData.serviceConfig.service;
|
||||
|
||||
namedEventBusName = `${serviceName}-named-event-bus`;
|
||||
arnEventBusName = `${serviceName}-arn-event-bus`;
|
||||
|
||||
// get default event bus ARN
|
||||
const defaultEventBusArn = (await describeEventBus('default')).Arn;
|
||||
|
||||
stackName = `${serviceName}-${stage}`;
|
||||
// create an external Event Bus
|
||||
// NOTE: deployment can only be done once the Event Bus is created
|
||||
arnEventBusArn = (await createEventBus(arnEventBusName)).EventBusArn;
|
||||
// update the YAML file with the arn
|
||||
await serviceData.updateConfig({
|
||||
functions: {
|
||||
eventBusDefaultArn: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: defaultEventBusArn,
|
||||
pattern: { source: ['serverless.test'] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
eventBusArn: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: arnEventBusArn,
|
||||
pattern: { source: ['serverless.test'] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
describe('AWS - Event Bridge Integration Test', () => {
|
||||
describe('Using deprecated CustomResource deployment pattern', function () {
|
||||
this.timeout(1000 * 60 * 100); // Involves time-taking deploys
|
||||
let serviceName;
|
||||
let stackName;
|
||||
let servicePath;
|
||||
let namedEventBusName;
|
||||
let arnEventBusName;
|
||||
let arnEventBusArn;
|
||||
const eventSource = 'serverless.test';
|
||||
const stage = 'dev';
|
||||
const putEventEntries = [
|
||||
{
|
||||
Source: eventSource,
|
||||
DetailType: 'ServerlessDetailType',
|
||||
Detail: '{"Key1":"Value1"}',
|
||||
},
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
const serviceData = await fixtures.setup('eventBridge');
|
||||
({ servicePath } = serviceData);
|
||||
serviceName = serviceData.serviceConfig.service;
|
||||
|
||||
namedEventBusName = `${serviceName}-named-event-bus`;
|
||||
arnEventBusName = `${serviceName}-arn-event-bus`;
|
||||
|
||||
// get default event bus ARN
|
||||
const defaultEventBusArn = (await describeEventBus('default')).Arn;
|
||||
|
||||
stackName = `${serviceName}-${stage}`;
|
||||
// create an external Event Bus
|
||||
// NOTE: deployment can only be done once the Event Bus is created
|
||||
arnEventBusArn = (await createEventBus(arnEventBusName)).EventBusArn;
|
||||
// update the YAML file with the arn
|
||||
await serviceData.updateConfig({
|
||||
functions: {
|
||||
eventBusDefaultArn: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: defaultEventBusArn,
|
||||
pattern: { source: [eventSource] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
eventBusArn: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: arnEventBusArn,
|
||||
pattern: { source: [eventSource] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
// deploy the service
|
||||
return deployService(servicePath);
|
||||
});
|
||||
// deploy the service
|
||||
return deployService(servicePath);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
log.notice('Removing service...');
|
||||
await removeService(servicePath);
|
||||
log.notice(`Deleting Event Bus "${arnEventBusName}"...`);
|
||||
return deleteEventBus(arnEventBusName);
|
||||
});
|
||||
after(async () => {
|
||||
log.notice('Removing service...');
|
||||
await removeService(servicePath);
|
||||
log.notice(`Deleting Event Bus "${arnEventBusName}"...`);
|
||||
return deleteEventBus(arnEventBusName);
|
||||
});
|
||||
|
||||
describe('Default Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', () => {
|
||||
const functionName = 'eventBusDefault';
|
||||
const markers = getMarkers(functionName);
|
||||
describe('Default Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', async () => {
|
||||
const functionName = 'eventBusDefault';
|
||||
const markers = getMarkers(functionName);
|
||||
|
||||
return confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents('default', putEventEntries),
|
||||
{
|
||||
checkIsComplete: (events) =>
|
||||
events.find((event) => event.message.includes(markers.start)) &&
|
||||
events.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
).then((events) => {
|
||||
const events = await confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents('default', putEventEntries),
|
||||
{
|
||||
checkIsComplete: (data) =>
|
||||
data.find((event) => event.message.includes(markers.start)) &&
|
||||
data.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
);
|
||||
const logs = events.map((event) => event.message).join('\n');
|
||||
expect(logs).to.include(`"source":"${eventSource}"`);
|
||||
expect(logs).to.include(`"detail-type":"${putEventEntries[0].DetailType}"`);
|
||||
expect(logs).to.include(`"detail":${putEventEntries[0].Detail}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', async () => {
|
||||
const functionName = 'eventBusCustom';
|
||||
const markers = getMarkers(functionName);
|
||||
|
||||
const events = await confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents(namedEventBusName, putEventEntries),
|
||||
{
|
||||
checkIsComplete: (data) =>
|
||||
data.find((event) => event.message.includes(markers.start)) &&
|
||||
data.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
);
|
||||
const logs = events.map((event) => event.message).join('\n');
|
||||
expect(logs).to.include(`"source":"${eventSource}"`);
|
||||
expect(logs).to.include(`"detail-type":"${putEventEntries[0].DetailType}"`);
|
||||
expect(logs).to.include(`"detail":${putEventEntries[0].Detail}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Arn Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', async () => {
|
||||
const functionName = 'eventBusArn';
|
||||
const markers = getMarkers(functionName);
|
||||
|
||||
const events = await confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents(arnEventBusName, putEventEntries),
|
||||
{
|
||||
checkIsComplete: (data) =>
|
||||
data.find((event) => event.message.includes(markers.start)) &&
|
||||
data.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
);
|
||||
const logs = events.map((event) => event.message).join('\n');
|
||||
expect(logs).to.include(`"source":"${eventSource}"`);
|
||||
expect(logs).to.include(`"detail-type":"${putEventEntries[0].DetailType}"`);
|
||||
@ -105,42 +148,135 @@ describe('AWS - Event Bridge Integration Test', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', () => {
|
||||
const functionName = 'eventBusCustom';
|
||||
const markers = getMarkers(functionName);
|
||||
describe('Using native CloudFormation deployment pattern', function () {
|
||||
this.timeout(1000 * 60 * 10); // Involves time-taking deploys
|
||||
let serviceName;
|
||||
let stackName;
|
||||
let servicePath;
|
||||
let namedEventBusName;
|
||||
let arnEventBusName;
|
||||
let arnEventBusArn;
|
||||
const eventSource = 'serverless.test';
|
||||
const stage = 'dev';
|
||||
const putEventEntries = [
|
||||
{
|
||||
Source: eventSource,
|
||||
DetailType: 'ServerlessDetailType',
|
||||
Detail: '{"Key1":"Value1"}',
|
||||
},
|
||||
];
|
||||
|
||||
return confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents(namedEventBusName, putEventEntries),
|
||||
{
|
||||
checkIsComplete: (events) =>
|
||||
events.find((event) => event.message.includes(markers.start)) &&
|
||||
events.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
).then((events) => {
|
||||
before(async () => {
|
||||
const serviceData = await fixtures.setup('eventBridge');
|
||||
({ servicePath } = serviceData);
|
||||
serviceName = serviceData.serviceConfig.service;
|
||||
|
||||
namedEventBusName = `${serviceName}-named-event-bus`;
|
||||
arnEventBusName = `${serviceName}-arn-event-bus`;
|
||||
|
||||
// get default event bus ARN
|
||||
const defaultEventBusArn = (await describeEventBus('default')).Arn;
|
||||
|
||||
stackName = `${serviceName}-${stage}`;
|
||||
// create an external Event Bus
|
||||
// NOTE: deployment can only be done once the Event Bus is created
|
||||
arnEventBusArn = (await createEventBus(arnEventBusName)).EventBusArn;
|
||||
await serviceData.updateConfig({
|
||||
provider: {
|
||||
eventBridge: {
|
||||
useCloudFormation: true,
|
||||
},
|
||||
},
|
||||
functions: {
|
||||
eventBusDefaultArn: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: defaultEventBusArn,
|
||||
pattern: { source: [eventSource] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
eventBusArn: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: arnEventBusArn,
|
||||
pattern: { source: [eventSource] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
return deployService(servicePath);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
log.notice('Removing service...');
|
||||
await removeService(servicePath);
|
||||
log.notice(`Deleting Event Bus "${arnEventBusName}"...`);
|
||||
return deleteEventBus(arnEventBusName);
|
||||
});
|
||||
|
||||
describe('Default Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', async () => {
|
||||
const functionName = 'eventBusDefault';
|
||||
const markers = getMarkers(functionName);
|
||||
|
||||
const events = await confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents('default', putEventEntries),
|
||||
{
|
||||
checkIsComplete: (data) =>
|
||||
data.find((event) => event.message.includes(markers.start)) &&
|
||||
data.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
);
|
||||
const logs = events.map((event) => event.message).join('\n');
|
||||
expect(logs).to.include(`"source":"${eventSource}"`);
|
||||
expect(logs).to.include(`"detail-type":"${putEventEntries[0].DetailType}"`);
|
||||
expect(logs).to.include(`"detail":${putEventEntries[0].Detail}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Arn Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', () => {
|
||||
const functionName = 'eventBusArn';
|
||||
const markers = getMarkers(functionName);
|
||||
describe('Custom Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', async () => {
|
||||
const functionName = 'eventBusCustom';
|
||||
const markers = getMarkers(functionName);
|
||||
|
||||
const events = await confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents(namedEventBusName, putEventEntries),
|
||||
{
|
||||
checkIsComplete: (data) =>
|
||||
data.find((event) => event.message.includes(markers.start)) &&
|
||||
data.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
);
|
||||
const logs = events.map((event) => event.message).join('\n');
|
||||
expect(logs).to.include(`"source":"${eventSource}"`);
|
||||
expect(logs).to.include(`"detail-type":"${putEventEntries[0].DetailType}"`);
|
||||
expect(logs).to.include(`"detail":${putEventEntries[0].Detail}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Arn Event Bus', () => {
|
||||
it('should invoke function when an event is sent to the event bus', async () => {
|
||||
const functionName = 'eventBusArn';
|
||||
const markers = getMarkers(functionName);
|
||||
|
||||
const events = await confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents(arnEventBusName, putEventEntries),
|
||||
{
|
||||
checkIsComplete: (data) =>
|
||||
data.find((event) => event.message.includes(markers.start)) &&
|
||||
data.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
);
|
||||
|
||||
return confirmCloudWatchLogs(
|
||||
`/aws/lambda/${stackName}-${functionName}`,
|
||||
() => putEvents(arnEventBusName, putEventEntries),
|
||||
{
|
||||
checkIsComplete: (events) =>
|
||||
events.find((event) => event.message.includes(markers.start)) &&
|
||||
events.find((event) => event.message.includes(markers.end)),
|
||||
}
|
||||
).then((events) => {
|
||||
const logs = events.map((event) => event.message).join('\n');
|
||||
expect(logs).to.include(`"source":"${eventSource}"`);
|
||||
expect(logs).to.include(`"detail-type":"${putEventEntries[0].DetailType}"`);
|
||||
|
||||
@ -983,4 +983,28 @@ describe('#naming()', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEventBridgeEventBusLogicalId()', () => {
|
||||
it('should normalize the event bus name and append correct suffix', () => {
|
||||
expect(sdk.naming.getEventBridgeEventBusLogicalId('ExampleEventBusName')).to.equal(
|
||||
'ExampleEventBusNameEventBridgeEventBus'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEventBridgeRuleLogicalId()', () => {
|
||||
it('should normalize the rule name and append correct suffix', () => {
|
||||
expect(sdk.naming.getEventBridgeRuleLogicalId('exampleRuleName')).to.equal(
|
||||
'ExampleRuleNameEventBridgeRule'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEventBridgeLambdaPermissionLogicalId()', () => {
|
||||
it('should normalize the name and append correct suffix with index', () => {
|
||||
expect(sdk.naming.getEventBridgeLambdaPermissionLogicalId('exampleFunction', 1)).to.equal(
|
||||
'ExampleFunctionEventBridgeLambdaPermission1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
const chai = require('chai');
|
||||
const runServerless = require('../../../../../../../../utils/run-serverless');
|
||||
|
||||
const { expect } = chai;
|
||||
chai.use(require('chai-as-promised'));
|
||||
chai.use(require('sinon-chai'));
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
const NAME_OVER_64_CHARS = 'oneVeryLongAndVeryStrangeAndVeryComplicatedFunctionNameOver64Chars';
|
||||
|
||||
@ -128,104 +131,362 @@ const serverlessConfigurationExtension = {
|
||||
};
|
||||
|
||||
describe('EventBridgeEvents', () => {
|
||||
let cfResources;
|
||||
let naming;
|
||||
describe('using custom resources deployment pattern', () => {
|
||||
let cfResources;
|
||||
let naming;
|
||||
|
||||
before(() =>
|
||||
runServerless({
|
||||
fixture: 'function',
|
||||
configExt: serverlessConfigurationExtension,
|
||||
cliArgs: ['package'],
|
||||
}).then(({ cfTemplate, awsNaming }) => {
|
||||
({ Resources: cfResources } = cfTemplate);
|
||||
before(async () => {
|
||||
const { cfTemplate, awsNaming } = await runServerless({
|
||||
fixture: 'function',
|
||||
configExt: serverlessConfigurationExtension,
|
||||
cliArgs: ['package'],
|
||||
});
|
||||
cfResources = cfTemplate.Resources;
|
||||
naming = awsNaming;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} id
|
||||
*/
|
||||
function getEventBridgeConfigById(resourceLogicalId) {
|
||||
const eventBridgeId = naming.getCustomResourceEventBridgeResourceLogicalId(
|
||||
resourceLogicalId,
|
||||
1
|
||||
);
|
||||
return cfResources[eventBridgeId].Properties.EventBridgeConfig;
|
||||
}
|
||||
function getEventBridgeConfigById(resourceLogicalId) {
|
||||
const eventBridgeId = naming.getCustomResourceEventBridgeResourceLogicalId(
|
||||
resourceLogicalId,
|
||||
1
|
||||
);
|
||||
return cfResources[eventBridgeId].Properties.EventBridgeConfig;
|
||||
}
|
||||
|
||||
it('should create the correct policy Statement', () => {
|
||||
const roleId = naming.getCustomResourcesRoleLogicalId('default', '12345');
|
||||
it('should create the correct policy Statement', () => {
|
||||
const roleId = naming.getCustomResourcesRoleLogicalId('default', '12345');
|
||||
|
||||
const [firstStatement, secondStatement, thirdStatment] = cfResources[
|
||||
roleId
|
||||
].Properties.Policies[0].PolicyDocument.Statement;
|
||||
expect(firstStatement.Effect).to.be.eq('Allow');
|
||||
expect(firstStatement.Resource['Fn::Join'][1]).to.deep.include('arn');
|
||||
expect(firstStatement.Resource['Fn::Join'][1]).to.deep.include('events');
|
||||
expect(firstStatement.Resource['Fn::Join'][1]).to.deep.include('event-bus/*');
|
||||
expect(firstStatement.Action).to.be.deep.eq(['events:CreateEventBus', 'events:DeleteEventBus']);
|
||||
const [firstStatement, secondStatement, thirdStatment] = cfResources[
|
||||
roleId
|
||||
].Properties.Policies[0].PolicyDocument.Statement;
|
||||
expect(firstStatement.Effect).to.be.eq('Allow');
|
||||
expect(firstStatement.Resource['Fn::Join'][1]).to.deep.include('arn');
|
||||
expect(firstStatement.Resource['Fn::Join'][1]).to.deep.include('events');
|
||||
expect(firstStatement.Resource['Fn::Join'][1]).to.deep.include('event-bus/*');
|
||||
expect(firstStatement.Action).to.be.deep.eq([
|
||||
'events:CreateEventBus',
|
||||
'events:DeleteEventBus',
|
||||
]);
|
||||
|
||||
expect(secondStatement.Effect).to.be.eq('Allow');
|
||||
expect(secondStatement.Resource['Fn::Join'][1]).to.deep.include('events');
|
||||
expect(secondStatement.Resource['Fn::Join'][1]).to.deep.include('rule/*');
|
||||
expect(secondStatement.Action).to.be.deep.eq([
|
||||
'events:PutRule',
|
||||
'events:RemoveTargets',
|
||||
'events:PutTargets',
|
||||
'events:DeleteRule',
|
||||
]);
|
||||
expect(secondStatement.Effect).to.be.eq('Allow');
|
||||
expect(secondStatement.Resource['Fn::Join'][1]).to.deep.include('events');
|
||||
expect(secondStatement.Resource['Fn::Join'][1]).to.deep.include('rule/*');
|
||||
expect(secondStatement.Action).to.be.deep.eq([
|
||||
'events:PutRule',
|
||||
'events:RemoveTargets',
|
||||
'events:PutTargets',
|
||||
'events:DeleteRule',
|
||||
]);
|
||||
|
||||
expect(thirdStatment.Effect).to.be.eq('Allow');
|
||||
expect(thirdStatment.Resource['Fn::Join'][1]).to.deep.include('function');
|
||||
expect(thirdStatment.Resource['Fn::Join'][1]).to.deep.include('lambda');
|
||||
expect(thirdStatment.Action).to.be.deep.eq(['lambda:AddPermission', 'lambda:RemovePermission']);
|
||||
});
|
||||
it('should create the necessary resource', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('default');
|
||||
expect(eventBridgeConfig.RuleName).to.include('dev-default-rule-1');
|
||||
});
|
||||
expect(thirdStatment.Effect).to.be.eq('Allow');
|
||||
expect(thirdStatment.Resource['Fn::Join'][1]).to.deep.include('function');
|
||||
expect(thirdStatment.Resource['Fn::Join'][1]).to.deep.include('lambda');
|
||||
expect(thirdStatment.Action).to.be.deep.eq([
|
||||
'lambda:AddPermission',
|
||||
'lambda:RemovePermission',
|
||||
]);
|
||||
});
|
||||
it('should create the necessary resource', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('default');
|
||||
expect(eventBridgeConfig.RuleName).to.include('dev-default-rule-1');
|
||||
});
|
||||
|
||||
it("should ensure rule name doesn't exceed 64 chars", () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById(NAME_OVER_64_CHARS);
|
||||
expect(eventBridgeConfig.RuleName.endsWith('rule-1')).to.be.true;
|
||||
expect(eventBridgeConfig.RuleName).lengthOf.lte(64);
|
||||
});
|
||||
it("should ensure rule name doesn't exceed 64 chars", () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById(NAME_OVER_64_CHARS);
|
||||
expect(eventBridgeConfig.RuleName.endsWith('rule-1')).to.be.true;
|
||||
expect(eventBridgeConfig.RuleName).lengthOf.lte(64);
|
||||
});
|
||||
|
||||
it('should support input configuration', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('configureInput');
|
||||
expect(eventBridgeConfig.Input.key1).be.eq('value1');
|
||||
expect(eventBridgeConfig.Input.key2).be.deep.eq({
|
||||
nested: 'value2',
|
||||
it('should support input configuration', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('configureInput');
|
||||
expect(eventBridgeConfig.Input.key1).be.eq('value1');
|
||||
expect(eventBridgeConfig.Input.key2).be.deep.eq({
|
||||
nested: 'value2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support arn at eventBus', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('configureInput');
|
||||
expect(eventBridgeConfig.EventBus).be.eq(
|
||||
'arn:aws:events:us-east-1:12345:event-bus/some-event-bus'
|
||||
);
|
||||
});
|
||||
it('should support inputPath configuration', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('inputPathConfiguration');
|
||||
expect(eventBridgeConfig.InputPath).be.eq('$.stageVariables');
|
||||
});
|
||||
|
||||
it('should support inputTransformer configuration', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('inputTransformer');
|
||||
const {
|
||||
InputTemplate,
|
||||
InputPathsMap: { eventTime },
|
||||
} = eventBridgeConfig.InputTransformer;
|
||||
expect(InputTemplate).be.eq('{"time": <eventTime>, "key1": "value1"}');
|
||||
expect(eventTime).be.eq('$.time');
|
||||
});
|
||||
|
||||
it('should register created and delete event bus permissions for non default event bus', () => {
|
||||
const roleId = naming.getCustomResourcesRoleLogicalId('customSaas', '12345');
|
||||
const [firstStatement] = cfResources[roleId].Properties.Policies[0].PolicyDocument.Statement;
|
||||
expect(firstStatement.Action[0]).to.be.eq('events:CreateEventBus');
|
||||
expect(firstStatement.Action[1]).to.be.eq('events:DeleteEventBus');
|
||||
expect(firstStatement.Effect).to.be.eq('Allow');
|
||||
});
|
||||
|
||||
it('should fail when trying to reference event bus via CF intrinsic function', async () => {
|
||||
await expect(
|
||||
runServerless({
|
||||
fixture: 'function',
|
||||
configExt: {
|
||||
functions: {
|
||||
foo: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: { Ref: 'ImportedEventBus' },
|
||||
schedule: 'rate(10 minutes)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cliArgs: ['package'],
|
||||
})
|
||||
).to.be.eventually.rejected.and.have.property(
|
||||
'code',
|
||||
'ERROR_INVALID_REFERENCE_TO_EVENT_BUS_CUSTOM_RESOURCE'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support arn at eventBus', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('configureInput');
|
||||
expect(eventBridgeConfig.EventBus).be.eq(
|
||||
'arn:aws:events:us-east-1:12345:event-bus/some-event-bus'
|
||||
);
|
||||
});
|
||||
it('should support inputPath configuration', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('inputPathConfiguration');
|
||||
expect(eventBridgeConfig.InputPath).be.eq('$.stageVariables');
|
||||
});
|
||||
describe('using native CloudFormation', () => {
|
||||
describe('when event bus is created as a part of the stack', () => {
|
||||
let cfResources;
|
||||
let naming;
|
||||
let eventBusLogicalId;
|
||||
let ruleResource;
|
||||
let ruleTarget;
|
||||
let inputPathRuleTarget;
|
||||
let inputTransformerRuleTarget;
|
||||
const schedule = 'rate(10 minutes)';
|
||||
const eventBusName = 'nondefault';
|
||||
const pattern = {
|
||||
source: ['aws.cloudformation'],
|
||||
};
|
||||
const input = {
|
||||
key1: 'value1',
|
||||
key2: {
|
||||
nested: 'value2',
|
||||
},
|
||||
};
|
||||
const inputPath = '$.stageVariables';
|
||||
const inputTransformer = {
|
||||
inputTemplate: '{"time": <eventTime>, "key1": "value1"}',
|
||||
inputPathsMap: {
|
||||
eventTime: '$.time',
|
||||
},
|
||||
};
|
||||
|
||||
it('should support inputTransformer configuration', () => {
|
||||
const eventBridgeConfig = getEventBridgeConfigById('inputTransformer');
|
||||
const {
|
||||
InputTemplate,
|
||||
InputPathsMap: { eventTime },
|
||||
} = eventBridgeConfig.InputTransformer;
|
||||
expect(InputTemplate).be.eq('{"time": <eventTime>, "key1": "value1"}');
|
||||
expect(eventTime).be.eq('$.time');
|
||||
});
|
||||
before(async () => {
|
||||
const { cfTemplate, awsNaming } = await runServerless({
|
||||
fixture: 'function',
|
||||
configExt: {
|
||||
provider: {
|
||||
eventBridge: {
|
||||
useCloudFormation: true,
|
||||
},
|
||||
},
|
||||
functions: {
|
||||
foo: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: eventBusName,
|
||||
schedule,
|
||||
pattern,
|
||||
input,
|
||||
},
|
||||
},
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: eventBusName,
|
||||
schedule,
|
||||
pattern,
|
||||
inputPath,
|
||||
},
|
||||
},
|
||||
{
|
||||
eventBridge: {
|
||||
eventBus: eventBusName,
|
||||
schedule,
|
||||
pattern,
|
||||
inputTransformer,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cliArgs: ['package'],
|
||||
});
|
||||
cfResources = cfTemplate.Resources;
|
||||
naming = awsNaming;
|
||||
eventBusLogicalId = naming.getEventBridgeEventBusLogicalId(eventBusName);
|
||||
ruleResource = Object.values(cfResources).find(
|
||||
(resource) =>
|
||||
resource.Type === 'AWS::Events::Rule' && resource.Properties.Name.endsWith('1')
|
||||
);
|
||||
ruleTarget = ruleResource.Properties.Targets[0];
|
||||
const inputPathRuleResource = Object.values(cfResources).find(
|
||||
(resource) =>
|
||||
resource.Type === 'AWS::Events::Rule' && resource.Properties.Name.endsWith('2')
|
||||
);
|
||||
inputPathRuleTarget = inputPathRuleResource.Properties.Targets[0];
|
||||
const inputTransformerRuleResource = Object.values(cfResources).find(
|
||||
(resource) =>
|
||||
resource.Type === 'AWS::Events::Rule' && resource.Properties.Name.endsWith('3')
|
||||
);
|
||||
inputTransformerRuleTarget = inputTransformerRuleResource.Properties.Targets[0];
|
||||
});
|
||||
|
||||
it('should register created and delete event bus permissions for non default event bus', () => {
|
||||
const roleId = naming.getCustomResourcesRoleLogicalId('customSaas', '12345');
|
||||
const [firstStatement] = cfResources[roleId].Properties.Policies[0].PolicyDocument.Statement;
|
||||
expect(firstStatement.Action[0]).to.be.eq('events:CreateEventBus');
|
||||
expect(firstStatement.Action[1]).to.be.eq('events:DeleteEventBus');
|
||||
expect(firstStatement.Effect).to.be.eq('Allow');
|
||||
it('should create an EventBus resource', () => {
|
||||
expect(cfResources[eventBusLogicalId].Properties).to.deep.equal({ Name: eventBusName });
|
||||
});
|
||||
|
||||
it('should correctly set ScheduleExpression on a created rule', () => {
|
||||
expect(ruleResource.Properties.ScheduleExpression).to.equal('rate(10 minutes)');
|
||||
});
|
||||
|
||||
it('should correctly set EventPattern on a created rule', () => {
|
||||
expect(ruleResource.Properties.EventPattern).to.deep.equal(JSON.stringify(pattern));
|
||||
});
|
||||
|
||||
it('should correctly set Input on the target for the created rule', () => {
|
||||
expect(ruleTarget.Input).to.deep.equal(JSON.stringify(input));
|
||||
});
|
||||
|
||||
it('should correctly set InputPath on the target for the created rule', () => {
|
||||
expect(inputPathRuleTarget.InputPath).to.deep.equal(inputPath);
|
||||
});
|
||||
|
||||
it('should correctly set InputTransformer on the target for the created rule', () => {
|
||||
expect(inputTransformerRuleTarget.InputTransformer.InputPathsMap).to.deep.equal(
|
||||
inputTransformer.inputPathsMap
|
||||
);
|
||||
expect(inputTransformerRuleTarget.InputTransformer.InputTemplate).to.deep.equal(
|
||||
inputTransformer.inputTemplate
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a rule that depends on created EventBus', () => {
|
||||
expect(ruleResource.DependsOn).to.equal(eventBusLogicalId);
|
||||
});
|
||||
|
||||
it('should create a rule that references correct function in target', () => {
|
||||
expect(ruleTarget.Arn['Fn::GetAtt'][0]).to.equal(naming.getLambdaLogicalId('foo'));
|
||||
});
|
||||
|
||||
it('should create a lambda permission resource that correctly references event bus in SourceArn', () => {
|
||||
const lambdaPermissionResource =
|
||||
cfResources[naming.getEventBridgeLambdaPermissionLogicalId('foo', 1)];
|
||||
|
||||
expect(
|
||||
lambdaPermissionResource.Properties.SourceArn['Fn::Join'][1][5]['Fn::Join'][1][1]
|
||||
).to.deep.equal(eventBusName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it references already existing EventBus or uses default one', () => {
|
||||
let cfResources;
|
||||
let naming;
|
||||
|
||||
before(async () => {
|
||||
const { cfTemplate, awsNaming } = await runServerless({
|
||||
fixture: 'function',
|
||||
cliArgs: ['package'],
|
||||
configExt: {
|
||||
provider: {
|
||||
eventBridge: {
|
||||
useCloudFormation: true,
|
||||
},
|
||||
},
|
||||
functions: {
|
||||
foo: {
|
||||
events: [
|
||||
{
|
||||
eventBridge: {
|
||||
schedule: 'rate(10 minutes)',
|
||||
eventBus: 'arn:xxxxx',
|
||||
},
|
||||
},
|
||||
{
|
||||
eventBridge: {
|
||||
schedule: 'rate(10 minutes)',
|
||||
eventBus: { Ref: 'ImportedEventBus' },
|
||||
},
|
||||
},
|
||||
{
|
||||
eventBridge: {
|
||||
schedule: 'rate(10 minutes)',
|
||||
eventBus: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
eventBridge: {
|
||||
schedule: 'rate(10 minutes)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
cfResources = cfTemplate.Resources;
|
||||
naming = awsNaming;
|
||||
});
|
||||
|
||||
it('should not create an EventBus if it is provided or default', async () => {
|
||||
expect(Object.values(cfResources).some((value) => value.Type === 'AWS::Events::EventBus'))
|
||||
.to.be.false;
|
||||
});
|
||||
|
||||
it('should create a lambda permission resource that correctly references arn event bus in SourceArn', () => {
|
||||
const lambdaPermissionResource =
|
||||
cfResources[naming.getEventBridgeLambdaPermissionLogicalId('foo', 1)];
|
||||
|
||||
expect(
|
||||
lambdaPermissionResource.Properties.SourceArn['Fn::Join'][1][5]['Fn::Join'][1][1]
|
||||
).to.deep.equal('arn:xxxxx');
|
||||
});
|
||||
|
||||
it('should create a lambda permission resource that correctly references CF event bus in SourceArn', () => {
|
||||
const lambdaPermissionResource =
|
||||
cfResources[naming.getEventBridgeLambdaPermissionLogicalId('foo', 2)];
|
||||
|
||||
expect(
|
||||
lambdaPermissionResource.Properties.SourceArn['Fn::Join'][1][5]['Fn::Join'][1][1]
|
||||
).to.deep.equal({ Ref: 'ImportedEventBus' });
|
||||
});
|
||||
|
||||
it('should create a lambda permission resource that correctly references explicit default event bus in SourceArn', () => {
|
||||
const lambdaPermissionResource =
|
||||
cfResources[naming.getEventBridgeLambdaPermissionLogicalId('foo', 3)];
|
||||
|
||||
expect(
|
||||
lambdaPermissionResource.Properties.SourceArn['Fn::Join'][1][5]['Fn::Join'][1][1]
|
||||
).to.equal('default');
|
||||
});
|
||||
|
||||
it('should create a lambda permission resource that correctly references implicit default event bus in SourceArn', () => {
|
||||
const lambdaPermissionResource =
|
||||
cfResources[naming.getEventBridgeLambdaPermissionLogicalId('foo', 4)];
|
||||
|
||||
expect(
|
||||
lambdaPermissionResource.Properties.SourceArn['Fn::Join'][1][5]['Fn::Join'][1]
|
||||
).not.to.include('default');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user