From a25cbad4d81bbafafb00366d8554b8e2312ad78c Mon Sep 17 00:00:00 2001 From: Christian Nuss Date: Mon, 18 Apr 2022 13:23:31 -0700 Subject: [PATCH] feat: add Eventbridge events (#477) --- README.md | 29 ++++++++++++++---- __tests__/unit.eventbridge.js | 27 ++++++++++++++++ __tests__/utils.js | 46 +++++++++++++++++++++++++++- src/configure.d.ts | 2 +- src/event-sources/aws/eventbridge.js | 18 +++++++++++ src/event-sources/index.js | 3 ++ src/event-sources/utils.js | 20 ++++++++++++ 7 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 __tests__/unit.eventbridge.js create mode 100644 src/event-sources/aws/eventbridge.js diff --git a/README.md b/README.md index 8b585f6c..8333af4c 100644 --- a/README.md +++ b/README.md @@ -171,9 +171,10 @@ serverlessExpress({ #### eventSourceRoutes -Introduced in `@vendia/serverless-express@4.4.0` native support for `aws:sns` and `aws:dynamodb` events were introduced. - -A single function can be configured to handle events from SNS and DynamoDB, as well as the previously supported events. +A single function can be configured to handle additional kinds of AWS events: + - SNS + - DynamoDB Streams + - EventBridge Events (formerlly CloudWatch Events) Assuming the following function configuration in `serverless.yml`: @@ -190,6 +191,10 @@ functions: - stream: type: dynamodb arn: arn:aws:dynamodb:us-east-1:012345678990:table/my-table/stream/2021-07-15T15:05:51.683 + - eventBridge: + pattern: + source: + - aws.cloudformation ``` And the following configuration: @@ -199,19 +204,31 @@ serverlessExpress({ app, eventSourceRoutes: { 'AWS_SNS': '/sns', - 'AWS_DYNAMODB': '/dynamodb' + 'AWS_DYNAMODB': '/dynamodb', + 'AWS_EVENTBRIDGE': '/eventbridge', + } +}) +``` + +Alternatively, to handle only SNS events (the keys in the map are **optional**) + +```js +serverlessExpress({ + app, + eventSourceRoutes: { + 'AWS_SNS': '/sns', } }) ``` -Events from SNS and DynamoDB will `POST` to the routes configured in Express to handle `/sns` and `/dynamodb`, -respectively. +Events will `POST` to the routes configured. Also, to ensure the events propagated from an internal event and not externally, it is **highly recommended** to ensure the `Host` header matches: - SNS: `sns.amazonaws.com` - DynamoDB: `dynamodb.amazonaws.com` + - EventBridge: `events.amazonaws.com` ### logSettings diff --git a/__tests__/unit.eventbridge.js b/__tests__/unit.eventbridge.js new file mode 100644 index 00000000..18d8dc85 --- /dev/null +++ b/__tests__/unit.eventbridge.js @@ -0,0 +1,27 @@ +const eventSources = require('../src/event-sources') +const testUtils = require('./utils') + +const dynamodbEventSource = eventSources.getEventSource({ + eventSourceName: 'AWS_EVENTBRIDGE' +}) + +test('request is correct', () => { + const req = getReq({ event: testUtils.eventbridgeEvent }) + expect(typeof req).toEqual('object') + expect(req.headers).toEqual({ host: 'events.amazonaws.com' }) + expect(req.body).toEqual(testUtils.eventbridgeEvent) + expect(req.method).toEqual('POST') +}) + +test('request is correct (scheduled)', () => { + const req = getReq({ event: testUtils.eventbridgeScheduledEvent }) + expect(typeof req).toEqual('object') + expect(req.headers).toEqual({ host: 'events.amazonaws.com' }) + expect(req.body).toEqual(testUtils.eventbridgeScheduledEvent) + expect(req.method).toEqual('POST') +}) + +function getReq ({ event }) { + const request = dynamodbEventSource.getRequest({ event }) + return request +} diff --git a/__tests__/utils.js b/__tests__/utils.js index 9b9e4cb5..4a11e6f2 100644 --- a/__tests__/utils.js +++ b/__tests__/utils.js @@ -140,6 +140,38 @@ const snsEvent = { ] } +// Sample event from https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html +const eventbridgeEvent = { + version: '0', + id: 'fe8d3c65-xmpl-c5c3-2c87-81584709a377', + 'detail-type': 'RDS DB Instance Event', + source: 'aws.rds', + account: '123456789012', + time: '2020-04-28T07:20:20Z', + region: 'us-east-2', + resources: ['arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1'], + detail: { + EventCategories: ['backup'], + SourceType: 'DB_INSTANCE', + SourceArn: 'arn:aws:rds:us-east-2:123456789012:db:rdz6xmpliljlb1', + Date: '2020-04-28T07:20:20.112Z', + Message: 'Finished DB Instance backup', + SourceIdentifier: 'rdz6xmpliljlb1' + } +} + +const eventbridgeScheduledEvent = { + version: '0', + account: '123456789012', + region: 'us-east-2', + detail: {}, + 'detail-type': 'Scheduled Event', + source: 'aws.events', + time: '2019-03-01T01:23:45Z', + id: 'cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', + resources: ['arn:aws:events:us-east-2:123456789012:rule/my-schedule'] +} + describe('getEventSourceNameBasedOnEvent', () => { test('throws error on empty event', () => { expect(() => getEventSourceNameBasedOnEvent({ event: {} })).toThrow( @@ -161,10 +193,22 @@ describe('getEventSourceNameBasedOnEvent', () => { const result = getEventSourceNameBasedOnEvent({ event: snsEvent }) expect(result).toEqual('AWS_SNS') }) + + test('recognizes eventbridge event', () => { + const result = getEventSourceNameBasedOnEvent({ event: eventbridgeEvent }) + expect(result).toEqual('AWS_EVENTBRIDGE') + }) + + test('recognizes eventbridge scheduled event', () => { + const result = getEventSourceNameBasedOnEvent({ event: eventbridgeScheduledEvent }) + expect(result).toEqual('AWS_EVENTBRIDGE') + }) }) module.exports = { samHttpApiEvent, dynamoDbEvent, - snsEvent + snsEvent, + eventbridgeEvent, + eventbridgeScheduledEvent } diff --git a/src/configure.d.ts b/src/configure.d.ts index 620a13f6..c8af6cca 100644 --- a/src/configure.d.ts +++ b/src/configure.d.ts @@ -3,7 +3,7 @@ import { Handler } from "aws-lambda"; import Logger from "./logger"; import Framework from "./frameworks"; -type EventSources = "AWS_SNS" | "AWS_DYNAMODB"; +type EventSources = "AWS_SNS" | "AWS_DYNAMODB" | "AWS_EVENTBRIDGE"; interface EventSource { getRequest?: any; // TODO: diff --git a/src/event-sources/aws/eventbridge.js b/src/event-sources/aws/eventbridge.js new file mode 100644 index 00000000..1b719fd0 --- /dev/null +++ b/src/event-sources/aws/eventbridge.js @@ -0,0 +1,18 @@ +const { emptyResponseMapper } = require('../utils') + +const getRequestValuesFromEventBridge = ({ event }) => { + const method = 'POST' + const headers = { host: 'events.amazonaws.com' } + const body = event + + return { + method, + headers, + body + } +} + +module.exports = { + getRequest: getRequestValuesFromEventBridge, + getResponse: emptyResponseMapper +} diff --git a/src/event-sources/index.js b/src/event-sources/index.js index 3b8b3566..42c067fe 100644 --- a/src/event-sources/index.js +++ b/src/event-sources/index.js @@ -4,6 +4,7 @@ const awsAlbEventSource = require('./aws/alb') const awsLambdaEdgeEventSource = require('./aws/lambda-edge') const awsSnsEventSource = require('./aws/sns') const awsDynamoDbEventSource = require('./aws/dynamodb') +const awsEventBridgeEventSource = require('./aws/eventbridge') function getEventSource ({ eventSourceName }) { switch (eventSourceName) { @@ -19,6 +20,8 @@ function getEventSource ({ eventSourceName }) { return awsDynamoDbEventSource case 'AWS_SNS': return awsSnsEventSource + case 'AWS_EVENTBRIDGE': + return awsEventBridgeEventSource default: throw new Error('Couldn\'t detect valid event source.') } diff --git a/src/event-sources/utils.js b/src/event-sources/utils.js index ce525c73..6cbbcb09 100644 --- a/src/event-sources/utils.js +++ b/src/event-sources/utils.js @@ -84,6 +84,26 @@ function getEventSourceNameBasedOnEvent ({ if (event.requestContext) { return event.version === '2.0' ? 'AWS_API_GATEWAY_V2' : 'AWS_API_GATEWAY_V1' } + if ( + event.version && + event.version === '0' && + event.id && + event['detail-type'] && + event.source && + event.source.startsWith('aws.') && // Might need to adjust this for "Partner Sources", e.g. Auth0, Datadog, etc + event.account && + event.time && + event.region && + event.resources && + Array.isArray(event.resources) && + event.detail && + typeof event.detail === 'object' && + !Array.isArray(event.detail) + ) { + // AWS doesn't have a defining Event Source here, so we're being incredibly selective on the structure + // Ref: https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html + return 'AWS_EVENTBRIDGE' + } throw new Error('Unable to determine event source based on event.') }