diff --git a/docs/providers/aws/events/event-bridge.md b/docs/providers/aws/events/event-bridge.md index 904beb208..39c7c4b60 100644 --- a/docs/providers/aws/events/event-bridge.md +++ b/docs/providers/aws/events/event-bridge.md @@ -186,3 +186,47 @@ functions: eventTime: '$.time' inputTemplate: '{"time": , "key1": "value1"}' ``` + +## Adding a DLQ to an event rule + +DeadLetterConfig is not available for custom resources, only for native CloudFormation. + +```yml +functions: + myFunction: + handler: index.handler + events: + - eventBridge: + eventBus: custom-saas-events + pattern: + source: + - saas.external + deadLetterConfig: + targetArn: + Fn::GetAtt: + - QueueName + - Arn +``` + +## Adding a retry policy to an event rule + +RetryPolicy is not available for custom resources, only for native CloudFormation. + +```yml +functions: + myFunction: + handler: index.handler + events: + - eventBridge: + eventBus: custom-saas-events + pattern: + source: + - saas.external + deadLetterConfig: + Fn::GetAtt: + - QueueName + - Arn + retryPolicy: + maximumEventAge: 3600 + maximumRetryAttempts: 3 +``` diff --git a/lib/plugins/aws/package/compile/events/eventBridge/index.js b/lib/plugins/aws/package/compile/events/eventBridge/index.js index 6aa6de954..eabd94070 100644 --- a/lib/plugins/aws/package/compile/events/eventBridge/index.js +++ b/lib/plugins/aws/package/compile/events/eventBridge/index.js @@ -87,6 +87,30 @@ class AwsCompileEventBridgeEvents { required: ['inputTemplate'], additionalProperties: false, }, + retryPolicy: { + type: 'object', + properties: { + maximumEventAge: { + type: 'integer', + minimum: 60, + maximum: 86400, + }, + maximumRetryAttempts: { + type: 'integer', + minimum: 0, + maximum: 185, + }, + }, + }, + deadLetterConfig: { + type: 'object', + properties: { + targetArn: { + $ref: '#/definitions/awsArn', + }, + }, + additionalProperties: false, + }, }, anyOf: [{ required: ['pattern'] }, { required: ['schedule'] }], }); @@ -117,6 +141,9 @@ class AwsCompileEventBridgeEvents { const Input = event.eventBridge.input; const InputPath = event.eventBridge.inputPath; let InputTransformer = event.eventBridge.inputTransformer; + let RetryPolicy = event.eventBridge.retryPolicy; + let DeadLetterConfig = event.eventBridge.deadLetterConfig; + const RuleName = makeAndHashRuleName({ functionName: FunctionName, index: idx, @@ -144,6 +171,32 @@ class AwsCompileEventBridgeEvents { ); } + if (RetryPolicy) { + if (!shouldUseCloudFormation) { + throw new ServerlessError( + 'Configuring RetryPolicy 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_RETRY_POLICY_TO_EVENT_BUS_CUSTOM_RESOURCE' + ); + } + + RetryPolicy = { + MaximumEventAge: RetryPolicy.maximumEventAge, + MaximumRetryAttempts: RetryPolicy.maximumRetryAttempts, + }; + } + + if (DeadLetterConfig) { + if (!shouldUseCloudFormation) { + throw new ServerlessError( + 'Configuring DeadLetterConfig 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_DEAD_LETTER_CONFIG_TO_EVENT_BUS_CUSTOM_RESOURCE' + ); + } + DeadLetterConfig = { + TargetArn: DeadLetterConfig.targetArn, + }; + } + const eventBusName = EventBus; // Custom resources will be deprecated in next major release if (!shouldUseCloudFormation) { @@ -184,6 +237,8 @@ class AwsCompileEventBridgeEvents { idx, hasEventBusesIamRoleStatement, iamRoleStatements, + RetryPolicy, + DeadLetterConfig, }); } } @@ -297,6 +352,8 @@ class AwsCompileEventBridgeEvents { Pattern, Schedule, FunctionName, + RetryPolicy, + DeadLetterConfig, idx, }) { let eventBusResource; @@ -336,11 +393,13 @@ class AwsCompileEventBridgeEvents { Id: makeEventBusTargetId(RuleName), }; - const target = this.addInputConfigToTarget({ + const target = this.configureTarget({ target: targetBase, Input, InputPath, InputTransformer, + RetryPolicy, + DeadLetterConfig, }); // Create a rule @@ -449,7 +508,7 @@ class AwsCompileEventBridgeEvents { return null; } - addInputConfigToTarget({ target, Input, InputPath, InputTransformer }) { + configureTarget({ target, Input, InputPath, InputTransformer, RetryPolicy, DeadLetterConfig }) { if (Input) { target = Object.assign(target, { Input: JSON.stringify(Input), @@ -468,6 +527,21 @@ class AwsCompileEventBridgeEvents { }); return target; } + + if (RetryPolicy) { + target = Object.assign(target, { + RetryPolicy, + }); + return target; + } + + if (DeadLetterConfig) { + target = Object.assign(target, { + DeadLetterConfig, + }); + return target; + } + return target; } } diff --git a/test/unit/lib/plugins/aws/package/compile/events/eventBridge/index.test.js b/test/unit/lib/plugins/aws/package/compile/events/eventBridge/index.test.js index 7f355f9c9..18e347ff3 100644 --- a/test/unit/lib/plugins/aws/package/compile/events/eventBridge/index.test.js +++ b/test/unit/lib/plugins/aws/package/compile/events/eventBridge/index.test.js @@ -275,6 +275,71 @@ describe('EventBridgeEvents', () => { expect(firstStatement.Effect).to.be.eq('Allow'); }); + it('should fail when trying to set RetryPolicy', async () => { + await expect( + runServerless({ + fixture: 'function', + configExt: { + disabledDeprecations: ['AWS_EVENT_BRIDGE_CUSTOM_RESOURCE'], + functions: { + foo: { + events: [ + { + eventBridge: { + retryPolicy: { + maximumEventAge: 4200, + maximumRetryAttempts: 180, + }, + pattern: { + source: ['aws.something'], + }, + }, + }, + ], + }, + }, + }, + command: 'package', + }) + ).to.be.eventually.rejected.and.have.property( + 'code', + 'ERROR_INVALID_RETRY_POLICY_TO_EVENT_BUS_CUSTOM_RESOURCE' + ); + }); + + it('should fail when trying to set DeadLetterConfig', async () => { + await expect( + runServerless({ + fixture: 'function', + configExt: { + disabledDeprecations: ['AWS_EVENT_BRIDGE_CUSTOM_RESOURCE'], + functions: { + foo: { + events: [ + { + eventBridge: { + deadLetterConfig: { + targetArn: { + 'Fn::GetAtt': ['not-supported', 'Arn'], + }, + }, + pattern: { + source: ['aws.something'], + }, + }, + }, + ], + }, + }, + }, + command: 'package', + }) + ).to.be.eventually.rejected.and.have.property( + 'code', + 'ERROR_INVALID_DEAD_LETTER_CONFIG_TO_EVENT_BUS_CUSTOM_RESOURCE' + ); + }); + it('should fail when trying to reference event bus via CF intrinsic function', async () => { await expect( runServerless({ @@ -310,10 +375,6 @@ describe('EventBridgeEvents', () => { let eventBusLogicalId; let ruleResource; let ruleTarget; - let inputPathRuleTarget; - let inputTransformerRuleTarget; - let disabledRuleResource; - let enabledRuleResource; const schedule = 'rate(10 minutes)'; const eventBusName = 'nondefault'; const pattern = { @@ -332,6 +393,22 @@ describe('EventBridgeEvents', () => { eventTime: '$.time', }, }; + const retryPolicy = { + maximumEventAge: 7200, + maximumRetryAttempts: 9, + }; + + const deadLetterConfig = { + targetArn: { + 'Fn::GetAtt': ['test', 'Arn'], + }, + }; + + const getRuleResourceEndingWith = (resources, ending) => + Object.values(resources).find( + (resource) => + resource.Type === 'AWS::Events::Rule' && resource.Properties.Name.endsWith(ending) + ); before(async () => { const { cfTemplate, awsNaming } = await runServerless({ @@ -385,6 +462,22 @@ describe('EventBridgeEvents', () => { pattern, }, }, + { + eventBridge: { + eventBus: eventBusName, + schedule, + pattern, + retryPolicy, + }, + }, + { + eventBridge: { + eventBus: eventBusName, + schedule, + pattern, + deadLetterConfig, + }, + }, ], }, }, @@ -394,31 +487,8 @@ describe('EventBridgeEvents', () => { 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') - ); + ruleResource = getRuleResourceEndingWith(cfResources, '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]; - - disabledRuleResource = Object.values(cfResources).find( - (resource) => - resource.Type === 'AWS::Events::Rule' && resource.Properties.Name.endsWith('4') - ); - - enabledRuleResource = Object.values(cfResources).find( - (resource) => - resource.Type === 'AWS::Events::Rule' && resource.Properties.Name.endsWith('5') - ); }); it('should create an EventBus resource', () => { @@ -434,10 +504,12 @@ describe('EventBridgeEvents', () => { }); it('should correctly set State when disabled on a created rule', () => { + const disabledRuleResource = getRuleResourceEndingWith(cfResources, '4'); expect(disabledRuleResource.Properties.State).to.equal('DISABLED'); }); it('should correctly set State when enabled on a created rule', () => { + const enabledRuleResource = getRuleResourceEndingWith(cfResources, '5'); expect(enabledRuleResource.Properties.State).to.equal('ENABLED'); }); @@ -450,10 +522,14 @@ describe('EventBridgeEvents', () => { }); it('should correctly set InputPath on the target for the created rule', () => { + const inputPathRuleResource = getRuleResourceEndingWith(cfResources, '2'); + const inputPathRuleTarget = inputPathRuleResource.Properties.Targets[0]; expect(inputPathRuleTarget.InputPath).to.deep.equal(inputPath); }); it('should correctly set InputTransformer on the target for the created rule', () => { + const inputTransformerRuleResource = getRuleResourceEndingWith(cfResources, '3'); + const inputTransformerRuleTarget = inputTransformerRuleResource.Properties.Targets[0]; expect(inputTransformerRuleTarget.InputTransformer.InputPathsMap).to.deep.equal( inputTransformer.inputPathsMap ); @@ -462,6 +538,21 @@ describe('EventBridgeEvents', () => { ); }); + it('should support retryPolicy configuration', () => { + const retryPolicyRuleTarget = getRuleResourceEndingWith(cfResources, '6').Properties + .Targets[0]; + expect(retryPolicyRuleTarget.RetryPolicy).to.deep.equal({ + MaximumEventAge: 7200, + MaximumRetryAttempts: 9, + }); + }); + + it('should support deadLetterConfig configuration', () => { + const deadLetterConfigRuleTarget = getRuleResourceEndingWith(cfResources, '7').Properties + .Targets[0]; + expect(deadLetterConfigRuleTarget.DeadLetterConfig).to.have.property('TargetArn'); + }); + it('should create a rule that depends on created EventBus', () => { expect(ruleResource.DependsOn).to.equal(eventBusLogicalId); });