Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

feat: subscriptionReaper #557

Merged
merged 12 commits into from
Feb 14, 2022
51 changes: 51 additions & 0 deletions serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,18 @@ functions:
environment:
ELASTICSEARCH_DOMAIN_ENDPOINT: !Join ['', ['https://', !GetAtt ElasticSearchDomain.DomainEndpoint]]
NUMBER_OF_SHARDS: !If [isDev, 1, 3] # 133 indices, one per resource types

subscriptionReaper:
carvantes marked this conversation as resolved.
Show resolved Hide resolved
timeout: 30
runtime: nodejs14.x
description: 'Scheduled Lambda to remove expired Subscriptions'
role: SubscriptionReaperRole
handler: src/subscriptions/index.reaperHandler
events:
- schedule: rate(5 minutes)
- enable: ${self:custom.enableSubscriptions} # will only run if opted into subscription feature
environment:
ENABLE_MULTI_TENANCY: !Ref EnableMultiTenancy

subscriptionsRestHook:
timeout: 20
Expand Down Expand Up @@ -595,6 +607,7 @@ resources:
- 'dynamodb:BatchWriteItem'
Resource:
- !GetAtt ResourceDynamoDBTableV2.Arn
- !Join ['', [!GetAtt ResourceDynamoDBTableV2.Arn, '/index/*']]
- !GetAtt ExportRequestDynamoDBTable.Arn
- Effect: Allow
Action:
Expand Down Expand Up @@ -769,6 +782,44 @@ resources:
- 'kms:GenerateDataKeyWithoutPlaintext'
Resource:
- !GetAtt ElasticSearchKMSKey.Arn
SubscriptionReaperRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service: 'lambda.amazonaws.com'
Action: 'sts:AssumeRole'
Policies:
- PolicyName: 'SubscriptionReaperPolicy'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:UpdateItem
- dynamodb:Query
Resource:
- !Join ['', [!GetAtt ResourceDynamoDBTableV2.Arn, '/index/*']]
- !GetAtt ResourceDynamoDBTableV2.Arn
- PolicyName: 'KMSPolicy'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'kms:Describe*'
- 'kms:Get*'
- 'kms:List*'
- 'kms:Encrypt'
- 'kms:Decrypt'
- 'kms:ReEncrypt*'
- 'kms:GenerateDataKey'
- 'kms:GenerateDataKeyWithoutPlaintext'
Resource:
- !GetAtt DynamodbKMSKey.Arn
DdbToEsDLQ:
Type: AWS::SQS::Queue
Properties:
Expand Down
12 changes: 12 additions & 0 deletions src/subscriptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*
*/

import reaperHandler from './subscriptionReaper';
import RestHookHandler from './restHook';
import { AllowListInfo, getAllowListInfo } from './allowListUtil';

Expand All @@ -12,3 +19,8 @@ const restHookHandler = new RestHookHandler({ enableMultitenancy });
exports.handler = async (event: any) => {
return restHookHandler.sendRestHookNotification(event, allowListPromise);
};

/**
* Custom lambda handler that handles deleting expired subscriptions.
*/
exports.reaperHandler = reaperHandler;
83 changes: 83 additions & 0 deletions src/subscriptions/subscriptionReaper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { DynamoDbDataService } from 'fhir-works-on-aws-persistence-ddb';
import reaperHandler from './subscriptionReaper';

jest.mock('fhir-works-on-aws-persistence-ddb');
describe('subscriptionReaper', () => {
test('no subscriptions to delete', async () => {
const expectedResponse: {
success: boolean;
message: string;
}[] = [];
const subResource = [
{
resourceType: 'Subscription',
id: 'sub1',
status: 'requested',
end: '2121-01-01T00:00:00Z',
},
];
const mockGetActiveSubscriptions = jest.fn();
DynamoDbDataService.prototype.getActiveSubscriptions = mockGetActiveSubscriptions;
mockGetActiveSubscriptions.mockResolvedValueOnce(subResource);
const actualResponse = await reaperHandler({});
expect(actualResponse).toEqual(expectedResponse);
});

test('subscriptions that have expired should be deleted', async () => {
const message = `Successfully deleted ResourceType: Subscription, Id: sub1, VersionId: 1`;
const expectedResponse: {
success: boolean;
message: string;
}[] = [
{
success: true,
message,
},
];
const subResource = [
{
resourceType: 'Subscription',
id: 'sub1',
status: 'requested',
end: '2021-01-01T00:00:00Z',
},
{
resourceType: 'Subscription',
id: 'sub2',
status: 'requested',
end: '2121-01-01T00:00:00Z',
},
];
const mockGetActiveSubscriptions = jest.fn();
const mockDeleteResource = jest.fn();
DynamoDbDataService.prototype.getActiveSubscriptions = mockGetActiveSubscriptions;
DynamoDbDataService.prototype.deleteResource = mockDeleteResource;
mockGetActiveSubscriptions.mockResolvedValueOnce(subResource);
mockDeleteResource.mockResolvedValueOnce([{ success: true, message }]);
const actualResponse = await reaperHandler({});
expect(actualResponse).toEqual(expectedResponse);
});

test('subscriptions that have no specified end should not be deleted', async () => {
const expectedResponse: {
success: boolean;
message: string;
}[] = [];
const subResource = [
{
resourceType: 'Subscription',
id: 'sub1',
status: 'requested',
},
];
const mockGetActiveSubscriptions = jest.fn();
DynamoDbDataService.prototype.getActiveSubscriptions = mockGetActiveSubscriptions;
mockGetActiveSubscriptions.mockResolvedValueOnce(subResource);
const actualResponse = await reaperHandler({});
expect(actualResponse).toEqual(expectedResponse);
});
});
46 changes: 46 additions & 0 deletions src/subscriptions/subscriptionReaper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*
*/
import { DynamoDbDataService, DynamoDb } from 'fhir-works-on-aws-persistence-ddb';

const enableMultiTenancy = process.env.ENABLE_MULTI_TENANCY === 'true';
const dbServiceWithTenancy = new DynamoDbDataService(DynamoDb, false, {
enableMultiTenancy,
carvantes marked this conversation as resolved.
Show resolved Hide resolved
});
const dbService = new DynamoDbDataService(DynamoDb);

const reaperHandler = async (event: any) => {
console.log('subscriptionReaper event', event);
const subscriptions = await dbService.getActiveSubscriptions({});
const currentTime = new Date();
// filter out subscriptions without a defined end time.
// check if subscription is past its end date (ISO format)
// example format of subscriptions: https://www.hl7.org/fhir/subscription-example.json.html
return Promise.all(
subscriptions
.filter((s: Record<string, any>) => {
// if s.end is undefined, new Date(s.end) will throw an error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe remove this comment. I don't think an error is thrown here

const date = new Date(s.end);
if (date.toString() === 'Invalid Date') {
console.log(`Skipping subscription ${s.id} since the end date is not in a valid format: ${s.end}`);
return false;
}
return currentTime >= date;
})
.map(async (subscription) => {
// delete the subscription as it has reached its end time
return dbServiceWithTenancy.deleteResource({
resourceType: subscription.resourceType,
// eslint-disable-next-line no-underscore-dangle
id: subscription._id,
// _tenantId is an internal field, and getActiveSubscriptions returns the raw Record<string, any>
// eslint-disable-next-line no-underscore-dangle
tenantId: subscription._tenantId,
});
}),
);
};

export default reaperHandler;