deep-package-manager
Version:
DEEP Package Manager
535 lines (439 loc) • 14.1 kB
JavaScript
/**
* Created by AlexanderC on 5/27/15.
*/
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.DynamoDBService = undefined;
var _AbstractService = require('./AbstractService');
var _deepCore = require('deep-core');
var _deepCore2 = _interopRequireDefault(_deepCore);
var _deepDb = require('deep-db');
var _deepDb2 = _interopRequireDefault(_deepDb);
var _SQSService = require('./SQSService');
var _LambdaService = require('./LambdaService');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* DynamoDB service
*/
class DynamoDBService extends _AbstractService.AbstractService {
/**
* @param {Array} args
*/
constructor(...args) {
super(...args);
}
/**
* @returns {String}
*/
name() {
return _deepCore2.default.AWS.Service.DYNAMO_DB;
}
/**
* @returns {String[]}
*/
static get AVAILABLE_REGIONS() {
return [_deepCore2.default.AWS.Region.ANY];
}
/**
* @param {Core.Generic.ObjectStorage} services
* @returns {DynamoDBService}
* @private
*/
_setup(services) {
this._createDbTables(this._rawModels, this._rawModelsSettings)(tablesNames => {
this._config.tablesNames = tablesNames;
this._ready = true;
});
return this;
}
/**
* @param {Core.Generic.ObjectStorage} services
* @returns {DynamoDBService}
* @private
*/
_postProvision(services) {
// @todo: implement!
if (this._isUpdate) {
this._readyTeardown = true;
return this;
}
this._readyTeardown = true;
return this;
}
/**
* @param {Core.Generic.ObjectStorage} services
* @returns {DynamoDBService}
* @private
*/
_postDeployProvision(services) {
/*this._attachEventualConsistencyAlarms(() => {
this._ready = true;
});*/
this._ready = true;
return this;
}
/**
* @returns {String}
*/
get _eventualConsistencyEndpoint() {
let globalsConfig = this.property.config.globals;
return globalsConfig.storage.eventualConsistency.offloaderEndpoint;
}
/**
* @param {Function} cb
* @returns {*}
* @private
* @deprecated
*/
_attachEventualConsistencyAlarms(cb) {
let offloadingBackendArn = null;
let offloadingBackendName = null;
let offloadQueuesNames = [];
try {
let offloadingEndpoint = this._eventualConsistencyEndpoint;
if (!offloadingEndpoint) {
throw new Error('Missing eventual consistency offloading endpoint from globals config');
}
let offloadEndpointParts = offloadingEndpoint.replace(/^@/, '').split(':');
let offloadMicroserviceIdentifier = offloadEndpointParts[0];
let offloadActionIdentifier = `${offloadEndpointParts[1]}-${offloadEndpointParts[2]}`;
let lambda = this.provisioning.lambda;
let lambdaConfig = this.provisioning.services.find(_LambdaService.LambdaService).config();
offloadingBackendName = lambdaConfig.names[offloadMicroserviceIdentifier][offloadActionIdentifier];
offloadingBackendArn = this._generateLambdaArn(lambda, offloadingBackendName);
let sqsConfig = this.provisioning.services.find(_SQSService.SQSService).config();
let offloadQueuesVector = Object.keys(sqsConfig.dbOffloadQueues);
for (let queueShortName in sqsConfig.queues) {
if (!sqsConfig.queues.hasOwnProperty(queueShortName) || offloadQueuesVector.indexOf(queueShortName) === -1) {
continue;
}
offloadQueuesNames.push(sqsConfig.queues[queueShortName].name);
}
if (offloadQueuesNames.length <= 0) {
return cb();
}
} catch (error) {
console.warn(error);
return cb();
}
console.debug(`Setting up eventual consistency using queues: ${offloadQueuesNames.join(', ')}`);
this._ensureSnsTopicsForEventualConsistency(offloadQueuesNames).then((...args) => {
console.debug('Ensure CloudWatch Alarms set up for eventual consistency');
this._ensureCloudWatchAlarmsForEventualConsistency(offloadQueuesNames, ...args).then((...args) => {
console.debug('Ensure eventual consistency offload backend subscribed to SNS topics');
this._ensureLambdasSubscribedToEventualConsistencyTopics(offloadingBackendArn, offloadingBackendName, offloadQueuesNames, ...args).then(cb).catch(error => {
console.warn(error);
cb();
});
}).catch(error => {
console.warn(error);
cb();
});
}).catch(error => {
console.warn(error);
cb();
});
}
/**
* @param {String} offloadingBackendArn
* @param {String} offloadingBackendName
* @param {String[]} offloadQueuesNames
* @param {Object} snsTopicsMapping
* @returns {Promise|*}
*/
_ensureLambdasSubscribedToEventualConsistencyTopics(offloadingBackendArn, offloadingBackendName, offloadQueuesNames, snsTopicsMapping) {
return new Promise((resolve, reject) => {
let sns = this.provisioning.sns;
let lambda = this.provisioning.lambda;
let cloudWatchEvents = this.provisioning.cloudWatchEvents;
let premises = offloadQueuesNames.map(topicName => {
return this._subscribeLambdaToSnsTopic(sns, lambda, cloudWatchEvents, topicName, offloadingBackendArn, offloadingBackendName, snsTopicsMapping[topicName]);
});
Promise.all(premises).then(resolve);
});
}
/**
* @param {*} sns
* @param {*} lambda
* @param {*} cloudWatchEvents
* @param {String} topicName
* @param {String} lambdaArn
* @param {String} lambdaName
* @param {String} topicArn
* @returns {Promise|*}
*/
_subscribeLambdaToSnsTopic(sns, lambda, cloudWatchEvents, topicName, lambdaArn, lambdaName, topicArn) {
return new Promise((resolve, reject) => {
let subscribePayload = {
TopicArn: topicArn,
Protocol: 'lambda',
Endpoint: lambdaArn
};
sns.subscribe(subscribePayload, error => {
if (error) {
console.warn(error);
}
let permissionsPayload = {
Action: `${_deepCore2.default.AWS.Service.LAMBDA}:InvokeFunction`,
Principal: _deepCore2.default.AWS.Service.identifier(_deepCore2.default.AWS.Service.SIMPLE_NOTIFICATION_SERVICE),
FunctionName: lambdaArn,
StatementId: lambdaName
};
lambda.addPermission(permissionsPayload, error => {
if (error && error.code !== 'ResourceConflictException') {
console.warn(error);
}
resolve();
});
});
});
}
/**
* @param {String[]} offloadQueuesNames
* @param {Object} snsTopicsMapping
* @returns {Promise|*}
*/
_ensureCloudWatchAlarmsForEventualConsistency(offloadQueuesNames, snsTopicsMapping) {
return new Promise((resolve, reject) => {
let cloudWatch = this.provisioning.cloudWatch;
let listAlarmsPayload = {
AlarmNames: offloadQueuesNames
};
cloudWatch.describeAlarms(listAlarmsPayload, (error, data) => {
if (error) {
return reject(error);
}
let existingAlarms = (data.MetricAlarms || []).map(alarm => alarm.AlarmName);
let premises = offloadQueuesNames.filter(alarmName => existingAlarms.indexOf(alarmName) === -1).map(alarmName => {
return this._createCloudWatchAlarmForEventualConsistency(cloudWatch, alarmName, snsTopicsMapping[alarmName]);
});
Promise.all(premises).then(() => {
resolve(snsTopicsMapping);
});
});
});
}
/**
* @param {*} cloudWatch
* @param {String} alarmName
* @param {String} snsTopicArn
* @returns {Promise|*}
*/
_createCloudWatchAlarmForEventualConsistency(cloudWatch, alarmName, snsTopicArn) {
return new Promise((resolve, reject) => {
let payload = {
AlarmName: alarmName,
ComparisonOperator: 'GreaterThanThreshold',
EvaluationPeriods: 1,
MetricName: 'ApproximateNumberOfMessagesVisible',
Namespace: 'AWS/SQS',
Period: 60,
Statistic: 'Minimum',
Threshold: 0.0,
ActionsEnabled: true,
AlarmActions: [snsTopicArn],
AlarmDescription: `DynamoDB eventual consistency data offload from queue ${alarmName}`,
Dimensions: [{
Name: 'QueueName',
Value: alarmName
}],
Unit: 'Count'
};
cloudWatch.putMetricAlarm(payload, error => {
if (error && error.code !== 'ResourceConflictException') {
console.warn(error);
}
resolve();
});
});
}
/**
* @param {String[]} offloadQueuesNames
* @returns {Promise|*}
*/
_ensureSnsTopicsForEventualConsistency(offloadQueuesNames) {
return new Promise((resolve, reject) => {
let sns = this.provisioning.sns;
let snsTopicsMapping = {};
offloadQueuesNames.forEach(queueName => {
snsTopicsMapping[queueName] = this._generateTopicArn(sns, queueName);
});
let premises = offloadQueuesNames.map(topicName => {
return this._createSnsTopicForEventualConsistency(sns, topicName);
});
Promise.all(premises).then(() => {
resolve(snsTopicsMapping);
});
});
}
/**
* @param {*} sns
* @param {String} topicName
* @returns {Promise|*}
*/
_createSnsTopicForEventualConsistency(sns, topicName) {
return new Promise((resolve, reject) => {
let payload = {
Name: topicName
};
sns.createTopic(payload, error => {
if (error && error.code !== 'ResourceConflictException') {
console.warn(error);
}
resolve();
});
});
}
/**
*
* @param {Object} lambda
* @param {String} functionId
* @returns {*}
* @private
*/
_generateLambdaArn(lambda, functionId) {
return `arn:aws:lambda:${lambda.config.region}:${this.awsAccountId}:function:${functionId}`;
}
/**
* @param {*} sns
* @param {String} topicName
* @returns {String}
*/
_generateTopicArn(sns, topicName) {
return `arn:aws:sns:${sns.config.region}:${this.awsAccountId}:${topicName}`;
}
/**
* @param {Object} models
* @param {Object} modelsSettings
* @returns {Function}
* @private
*/
_createDbTables(models, modelsSettings) {
let tablesNames = this.generateTableNames(models);
let tablesSettings = this.generateTableSettings(modelsSettings);
let missingTablesNames = [];
if (this._isUpdate) {
let tablesNamesVector = DynamoDBService._objectValues(tablesNames);
missingTablesNames = DynamoDBService._objectValues(this._config.tablesNames).filter(x => tablesNamesVector.indexOf(x) < 0);
}
let deepDb = new _deepDb2.default(models, tablesNames, !!this.property.accountMicroservice, this.property.config.nonPartitionedModels);
// @todo waiting for https://github.com/aws/aws-sdk-js/issues/710 to be fixed
deepDb._setVogelsDriver(this.provisioning.dynamoDB);
// @todo: move this functionality?
this._provisioning.db = deepDb;
return callback => {
for (let name in tablesSettings) {
if (!tablesSettings.hasOwnProperty(name)) {
continue;
}
console.debug(`DynamoDB model '${name}' -> ${JSON.stringify(tablesSettings[name])}`);
}
deepDb.assureTables(() => {
if (missingTablesNames.length <= 0) {
callback(tablesNames);
return;
}
this._removeMissingTables(missingTablesNames, () => {
callback(tablesNames);
});
}, tablesSettings);
};
}
/**
* @param {Object} obj
* @returns {Array}
* @private
*/
static _objectValues(obj) {
let values = [];
for (let tableId in obj) {
if (!obj.hasOwnProperty(tableId)) {
continue;
}
values.push(obj[tableId]);
}
return values;
}
/**
* @param {String[]} missingTablesNames
* @param {Function} callback
* @returns {DynamoDBService}
* @private
*/
_removeMissingTables(missingTablesNames, callback) {
console.debug(`Removing DynamoDB tables: ${missingTablesNames.join(', ')}`);
for (let i in missingTablesNames) {
if (!missingTablesNames.hasOwnProperty(i)) {
continue;
}
let tableName = missingTablesNames[i];
this.provisioning.dynamoDB.deleteTable({
TableName: tableName
}, (error, data) => {
if (error) {
console.error(`Error while deleting DynamoDB table ${tableName}: ${error}`);
}
});
}
// @todo: leave it async?
callback();
return this;
}
/**
* @returns {Object}
* @private
*/
get _rawModels() {
return this.provisioning.property.config.models;
}
/**
* @returns {Object}
* @private
*/
get _rawModelsSettings() {
return this.provisioning.property.config.modelsSettings;
}
/**
* @param {Object} models
* @returns {Object}
*/
generateTableNames(models) {
let tables = {};
for (let modelKey in models) {
if (!models.hasOwnProperty(modelKey)) {
continue;
}
let backendModels = models[modelKey];
for (let modelName in backendModels) {
if (!backendModels.hasOwnProperty(modelName)) {
continue;
}
tables[modelName] = this.generateAwsResourceName(modelName, _deepCore2.default.AWS.Service.DYNAMO_DB);
}
}
return tables;
}
/**
* @param {Object} modelsSettings
* @returns {Object}
*/
generateTableSettings(modelsSettings) {
let tables = {};
for (let modelKey in modelsSettings) {
if (!modelsSettings.hasOwnProperty(modelKey)) {
continue;
}
let backendModelsSettings = modelsSettings[modelKey];
for (let modelName in backendModelsSettings) {
if (!backendModelsSettings.hasOwnProperty(modelName)) {
continue;
}
tables[modelName] = backendModelsSettings[modelName];
}
}
return tables;
}
}
exports.DynamoDBService = DynamoDBService;