From 2ef9b20c673b88f7d7aa2ccc0d4c9a7dec779097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Bour=C3=A9?= Date: Tue, 7 Mar 2023 21:37:39 +0100 Subject: [PATCH] feat(dataSources): Add Support for EventBridge DataSource (#574) --- doc/dataSources.md | 17 +- .../__snapshots__/dataSources.test.ts.snap | 192 ++++++++++++++++++ src/__tests__/dataSources.test.ts | 72 +++++++ .../__snapshots__/datasources.test.ts.snap | 17 +- src/__tests__/validation/datasources.test.ts | 107 ++++++++++ src/resources/DataSource.ts | 23 +++ src/types/cloudFormation.ts | 6 +- src/types/plugin.ts | 10 + src/validation.ts | 19 ++ 9 files changed, 460 insertions(+), 3 deletions(-) diff --git a/doc/dataSources.md b/doc/dataSources.md index 1c2fe402..f52add76 100644 --- a/doc/dataSources.md +++ b/doc/dataSources.md @@ -105,7 +105,7 @@ appSync: ```yaml appSync: dataSources: - api: + myDatabase: type: 'RELATIONAL_DATABASE' config: databaseName: myDatabase @@ -124,6 +124,21 @@ appSync: - `serviceRoleArn`: The service role ARN for this DataSource. If not provided, a new one will be created. - `iamRoleStatements`: Statements to use for the generated IAM Role. If not provided, default statements will be used. +## EventBridge + +```yaml +appSync: + dataSources: + myEventBus: + type: 'AMAZON_EVENTBRIDGE' + confing: + eventBusArn: !GetAtt MyEventBus.Arn +``` + +### config + +- `eventBusArn`: The ARN of the event bus + ## NONE ```yaml diff --git a/src/__tests__/__snapshots__/dataSources.test.ts.snap b/src/__tests__/__snapshots__/dataSources.test.ts.snap index 68023f53..c715fefe 100644 --- a/src/__tests__/__snapshots__/dataSources.test.ts.snap +++ b/src/__tests__/__snapshots__/dataSources.test.ts.snap @@ -810,6 +810,198 @@ Object { } `; +exports[`DataSource EventBridge should generate Resource with default role 1`] = ` +Object { + "GraphQlDseventBridge": Object { + "Properties": Object { + "ApiId": Object { + "Fn::GetAtt": Array [ + "GraphQlApi", + "ApiId", + ], + }, + "Description": "My eventBridge bus", + "EventBridgeConfig": Object { + "EventBusArn": "arn:aws:events:us-east-1:123456789012:event-bus/default", + }, + "Name": "eventBridge", + "ServiceRoleArn": Object { + "Fn::GetAtt": Array [ + "GraphQlDseventBridgeRole", + "Arn", + ], + }, + "Type": "AMAZON_EVENTBRIDGE", + }, + "Type": "AWS::AppSync::DataSource", + }, + "GraphQlDseventBridgeRole": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "sts:AssumeRole", + ], + "Effect": "Allow", + "Principal": Object { + "Service": Array [ + "appsync.amazonaws.com", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "events:PutEvents", + ], + "Effect": "Allow", + "Resource": Array [ + "arn:aws:events:us-east-1:123456789012:event-bus/default", + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AppSync-Datasource-eventBridge", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, +} +`; + +exports[`DataSource EventBridge should generate default role with a Ref for the bus ARN 1`] = ` +Object { + "GraphQlDseventBridge": Object { + "Properties": Object { + "ApiId": Object { + "Fn::GetAtt": Array [ + "GraphQlApi", + "ApiId", + ], + }, + "Description": "My eventBridge bus", + "EventBridgeConfig": Object { + "EventBusArn": Object { + "Fn::GetAtt": Array [ + "MyEventBus", + "Arn", + ], + }, + }, + "Name": "eventBridge", + "ServiceRoleArn": Object { + "Fn::GetAtt": Array [ + "GraphQlDseventBridgeRole", + "Arn", + ], + }, + "Type": "AMAZON_EVENTBRIDGE", + }, + "Type": "AWS::AppSync::DataSource", + }, + "GraphQlDseventBridgeRole": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "sts:AssumeRole", + ], + "Effect": "Allow", + "Principal": Object { + "Service": Array [ + "appsync.amazonaws.com", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "events:PutEvents", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "MyEventBus", + "Arn", + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AppSync-Datasource-eventBridge", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, +} +`; + +exports[`DataSource EventBridge should generate default role with custom statement 1`] = ` +Object { + "GraphQlDseventBridgeRole": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "sts:AssumeRole", + ], + "Effect": "Allow", + "Principal": Object { + "Service": Array [ + "appsync.amazonaws.com", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "events:PutEvents", + ], + "Effect": "Allow", + "Resource": Array [ + "arn:aws:events:us-east-1:123456789012:event-bus/default", + "arn:aws:events:us-east-1:123456789012:event-bus/other", + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AppSync-Datasource-eventBridge", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, +} +`; + exports[`DataSource HTTP should generate Resource with IAM authorization config 1`] = ` Object { "GraphQlDsmyEndpoint": Object { diff --git a/src/__tests__/dataSources.test.ts b/src/__tests__/dataSources.test.ts index db72665e..2675fd9e 100644 --- a/src/__tests__/dataSources.test.ts +++ b/src/__tests__/dataSources.test.ts @@ -106,6 +106,78 @@ describe('DataSource', () => { }); }); + describe('EventBridge', () => { + it('should generate Resource with default role', () => { + const api = new Api(given.appSyncConfig(), plugin); + const dataSource = new DataSource(api, { + type: 'AMAZON_EVENTBRIDGE', + name: 'eventBridge', + description: 'My eventBridge bus', + config: { + eventBusArn: + 'arn:aws:events:us-east-1:123456789012:event-bus/default', + }, + }); + + expect(dataSource.compile()).toMatchSnapshot(); + }); + + it('should generate default role with a Ref for the bus ARN', () => { + const api = new Api(given.appSyncConfig(), plugin); + const dataSource = new DataSource(api, { + type: 'AMAZON_EVENTBRIDGE', + name: 'eventBridge', + description: 'My eventBridge bus', + config: { + eventBusArn: { 'Fn::GetAtt': ['MyEventBus', 'Arn'] }, + }, + }); + + expect(dataSource.compile()).toMatchSnapshot(); + }); + + it('should generate default role with custom statement', () => { + const api = new Api(given.appSyncConfig(), plugin); + const dataSource = new DataSource(api, { + type: 'AMAZON_EVENTBRIDGE', + name: 'eventBridge', + description: 'My eventBridge bus', + config: { + eventBusArn: + 'arn:aws:events:us-east-1:123456789012:event-bus/default', + iamRoleStatements: [ + { + Effect: 'Allow', + Action: ['events:PutEvents'], + Resource: [ + 'arn:aws:events:us-east-1:123456789012:event-bus/default', + 'arn:aws:events:us-east-1:123456789012:event-bus/other', + ], + }, + ], + }, + }); + + expect(dataSource.compileDataSourceIamRole()).toMatchSnapshot(); + }); + + it('should not generate default role when a service role arn is passed', () => { + const api = new Api(given.appSyncConfig(), plugin); + const dataSource = new DataSource(api, { + type: 'AMAZON_EVENTBRIDGE', + name: 'eventBridge', + description: 'My eventBridge bus', + config: { + eventBusArn: + 'arn:aws:events:us-east-1:123456789012:event-bus/default', + serviceRoleArn: 'arn:aws:iam:', + }, + }); + + expect(dataSource.compileDataSourceIamRole()).toBeUndefined(); + }); + }); + describe('AWS Lambda', () => { it('should generate Resource with default role', () => { const api = new Api(given.appSyncConfig(), plugin); diff --git a/src/__tests__/validation/__snapshots__/datasources.test.ts.snap b/src/__tests__/validation/__snapshots__/datasources.test.ts.snap index 818d4b5e..303e25c1 100644 --- a/src/__tests__/validation/__snapshots__/datasources.test.ts.snap +++ b/src/__tests__/validation/__snapshots__/datasources.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Basic Invalid should validate: Invalid Datasource 1`] = ` -"/dataSources/myDynamoSource1/type: must be one of AMAZON_DYNAMODB, AMAZON_OPENSEARCH_SERVICE, AWS_LAMBDA, HTTP, NONE, RELATIONAL_DATABASE +"/dataSources/myDynamoSource1/type: must be one of AMAZON_DYNAMODB, AMAZON_OPENSEARCH_SERVICE, AWS_LAMBDA, HTTP, NONE, RELATIONAL_DATABASE, AMAZON_EVENTBRIDGE /dataSources: contains invalid data source definitions" `; @@ -28,6 +28,21 @@ exports[`DynamoDB Invalid should validate: Missing config 1`] = ` /dataSources: contains invalid data source definitions" `; +exports[`EventBridge Invalid should not validate: Empty config 1`] = ` +"/dataSources/myEventBridgeSource1/config: must have required property 'eventBusArn' +/dataSources: contains invalid data source definitions" +`; + +exports[`EventBridge Invalid should not validate: Invalid config 1`] = ` +"/dataSources/myEventBridgeSource1/config/eventBusArn: must be a string or a CloudFormation intrinsic function +/dataSources: contains invalid data source definitions" +`; + +exports[`EventBridge Invalid should not validate: Missing config 1`] = ` +"/dataSources/myEventBridgeSource1: must have required property 'config' +/dataSources: contains invalid data source definitions" +`; + exports[`HTTP Invalid should validate: Empty config 1`] = ` "/dataSources/http1/config: must have required property 'endpoint' /dataSources: contains invalid data source definitions" diff --git a/src/__tests__/validation/datasources.test.ts b/src/__tests__/validation/datasources.test.ts index 1a92135d..c582c4de 100644 --- a/src/__tests__/validation/datasources.test.ts +++ b/src/__tests__/validation/datasources.test.ts @@ -199,6 +199,113 @@ describe('DynamoDB', () => { }); }); }); +describe('EventBridge', () => { + describe('Valid', () => { + const assertions = [ + { + name: 'Valid config', + config: { + dataSources: { + myDynamoSource1: { + type: 'AMAZON_EVENTBRIDGE', + config: { + eventBusArn: + 'arn:aws:events:us-east-1:123456789012:event-bus/my-event-bus', + }, + }, + }, + }, + }, + { + name: 'EventBusArn as Ref', + config: { + dataSources: { + myDynamoSource1: { + type: 'AMAZON_EVENTBRIDGE', + config: { + eventBusArn: { + 'Fn::GetAtt': ['MyEventBus', 'Arn'], + }, + }, + }, + }, + }, + }, + { + name: 'Valid config, as array of maps', + config: { + dataSources: [ + { + myDynamoSource1: { + type: 'AMAZON_EVENTBRIDGE', + config: { + eventBusArn: + 'arn:aws:events:us-east-1:123456789012:event-bus/my-event-bus', + }, + }, + }, + ], + }, + }, + ]; + + assertions.forEach((config) => { + it(`should validate a ${config.name}`, () => { + expect(validateConfig({ ...basicConfig, ...config.config })).toBe(true); + }); + }); + }); + + describe('Invalid', () => { + const assertions = [ + { + name: 'Missing config', + config: { + dataSources: { + myEventBridgeSource1: { + type: 'AMAZON_EVENTBRIDGE', + }, + }, + }, + }, + { + name: 'Empty config', + config: { + dataSources: { + myEventBridgeSource1: { + type: 'AMAZON_EVENTBRIDGE', + config: {}, + }, + }, + }, + }, + { + name: 'Invalid config', + config: { + dataSources: { + myEventBridgeSource1: { + type: 'AMAZON_EVENTBRIDGE', + config: { + eventBusArn: 1234, + }, + }, + }, + }, + }, + ]; + + assertions.forEach((config) => { + it(`should not validate: ${config.name}`, () => { + expect(function () { + validateConfig({ + ...basicConfig, + ...config.config, + }); + }).toThrowErrorMatchingSnapshot(); + }); + }); + }); +}); describe('Lambda', () => { describe('Valid', () => { diff --git a/src/resources/DataSource.ts b/src/resources/DataSource.ts index c31f3637..5e3fd5cd 100644 --- a/src/resources/DataSource.ts +++ b/src/resources/DataSource.ts @@ -11,6 +11,7 @@ import { DsHttpConfig, DsRelationalDbConfig, IamStatement, + DsEventBridgeConfig, } from '../types/plugin'; import { Api } from './Api'; @@ -47,6 +48,10 @@ export class DataSource { ); } else if (this.config.type === 'HTTP') { resource.Properties.HttpConfig = this.getHttpConfig(this.config); + } else if (this.config.type === 'AMAZON_EVENTBRIDGE') { + resource.Properties.EventBridgeConfig = this.getEventBridgeConfig( + this.config, + ); } const logicalId = this.api.naming.getDataSourceLogicalId(this.config.name); @@ -98,6 +103,14 @@ export class DataSource { } } + getEventBridgeConfig( + config: DsEventBridgeConfig, + ): CfnDataSource['Properties']['EventBridgeConfig'] { + return { + EventBusArn: config.config.eventBusArn, + }; + } + getOpenSearchConfig( config: DsOpenSearchConfig, ): CfnDataSource['Properties']['OpenSearchServiceConfig'] { @@ -404,6 +417,16 @@ export class DataSource { return [defaultESStatement]; } + case 'AMAZON_EVENTBRIDGE': { + // Allow PutEvents on the EventBridge bus + const defaultEventBridgeStatement: IamStatement = { + Action: ['events:PutEvents'], + Effect: 'Allow', + Resource: [this.config.config.eventBusArn], + }; + + return [defaultEventBridgeStatement]; + } } } } diff --git a/src/types/cloudFormation.ts b/src/types/cloudFormation.ts index 6db069a7..6100d4e9 100644 --- a/src/types/cloudFormation.ts +++ b/src/types/cloudFormation.ts @@ -39,7 +39,8 @@ export type CfnDataSource = { | 'AMAZON_OPENSEARCH_SERVICE' | 'NONE' | 'HTTP' - | 'RELATIONAL_DATABASE'; + | 'RELATIONAL_DATABASE' + | 'AMAZON_EVENTBRIDGE'; ServiceRoleArn?: string | IntrinsicFunction; LambdaConfig?: { LambdaFunctionArn: string | IntrinsicFunction; @@ -75,6 +76,9 @@ export type CfnDataSource = { }; }; }; + EventBridgeConfig?: { + EventBusArn: string | IntrinsicFunction; + }; }; }; diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 173e569a..84d6debb 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -205,6 +205,15 @@ export type DsDynamoDBConfig = { }; }; +export type DsEventBridgeConfig = { + type: 'AMAZON_EVENTBRIDGE'; + config: { + serviceRoleArn?: string | IntrinsicFunction; + iamRoleStatements?: IamStatement[]; + eventBusArn: string | IntrinsicFunction; + }; +}; + export type DsRelationalDbConfig = { type: 'RELATIONAL_DATABASE'; config: { @@ -279,6 +288,7 @@ export type DataSourceConfig = { | DsRelationalDbConfig | DsOpenSearchConfig | DsLambdaConfig + | DsEventBridgeConfig | DsNone ); diff --git a/src/validation.ts b/src/validation.ts index fd124e0e..abbf8fbe 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -19,6 +19,7 @@ const DATASOURCE_TYPES = [ 'HTTP', 'NONE', 'RELATIONAL_DATABASE', + 'AMAZON_EVENTBRIDGE', ] as const; export const appSyncSchema = { @@ -462,6 +463,17 @@ export const appSyncSchema = { }, required: ['config'], }, + else: { + if: { properties: { type: { const: 'AMAZON_EVENTBRIDGE' } } }, + then: { + properties: { + config: { + $ref: '#/definitions/datasourceEventBridgeConfig', + }, + }, + required: ['config'], + }, + }, }, }, }, @@ -603,6 +615,13 @@ export const appSyncSchema = { }, required: [], }, + datasourceEventBridgeConfig: { + type: 'object', + properties: { + eventBusArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + }, + required: ['eventBusArn'], + }, }, properties: { name: { type: 'string' },