feat(AWS EventBridge): Support deadLetterQueue and retryPolicy

Co-authored-by: JP Bochi <jpbochi@pm.me>
This commit is contained in:
Eve 2021-09-10 09:55:37 -03:00 committed by GitHub
parent 95d3024ef5
commit 130fb3838f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 239 additions and 30 deletions

View File

@ -186,3 +186,47 @@ functions:
eventTime: '$.time'
inputTemplate: '{"time": <eventTime>, "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
```

View File

@ -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;
}
}

View File

@ -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);
});