deep-package-manager
Version:
DEEP Package Manager
342 lines (271 loc) • 10.9 kB
JavaScript
/**
* Created by CCristi on 3/27/17.
*/
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.BalancedStrategy = undefined;
var _os = require('os');
var _os2 = _interopRequireDefault(_os);
var _CloudFrontEvent = require('../Service/Helpers/CloudFrontEvent');
var _RecordSetAction = require('../Service/Helpers/RecordSetAction');
var _AbstractStrategy = require('./AbstractStrategy');
var _CloudFrontService = require('../Service/CloudFrontService');
var _CNAMEResolver = require('../Service/Helpers/CNAMEResolver');
var _MissingCNAMEException = require('../Exception/MissingCNAMEException');
var _CNAMEAlreadyExistsException = require('../Exception/CNAMEAlreadyExistsException');
var _Prompt = require('../../Helpers/Terminal/Prompt');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
class BalancedStrategy extends _AbstractStrategy.AbstractStrategy {
/**
* @param {Object[]} args
*/
constructor(...args) {
super(...args);
this._blueRoute53Record = null;
}
/**
* @param {Number} percentage
* @returns {Promise}
*/
publish(percentage) {
let cloudFrontService = this.replication.cloudFrontService;
let blueDistribution = cloudFrontService.blueConfig();
console.info('Changing blue distribution CNAMEs to wildcarded');
return this._changeCloudFrontCNames(percentage).then(blueAliases => {
return (this.skipDNSActions ? this._askForBlue2ndDNSRecord() : this._createBlue2ndDNSRecord(blueAliases)).then(() => {
console.info('Creating 3rd cloudfront distribution for blue green traffic management');
return this._createTrafficManagerCfDistribution();
}).then(managerDistribution => {
return Promise.all([cloudFrontService.waitForDistributionDeployed(blueDistribution.id), cloudFrontService.waitForDistributionDeployed(managerDistribution.Id)]).then(() => managerDistribution);
}).then(managerDistribution => {
managerDistribution.Aliases = blueAliases;
return cloudFrontService.changeCloudFrontCNAMEs(managerDistribution.Id, blueAliases.Items).then(() => managerDistribution);
});
}).then(managerDistribution => {
this._config.balancerDistribution = managerDistribution;
if (this.skipDNSActions) {
console.info(`NOTE: Please change your DNS provider to point at distribution ${managerDistribution.DomainName}: `, JSON.stringify(managerDistribution, null, ' '));
return Promise.resolve();
}
return this._updateBlueMainDNSRecord(managerDistribution).then(() => {
console.debug('Route53 Changes have been applied. Please note that DNS changes propagates slowly.');
});
});
}
/**
* @param {Number} percentage
* @returns {Promise}
*/
update(percentage) {
this.parameters.percentage = percentage;
// recompile lambda with new percentage
return this.attachLambdaEdgeFunction(this.balancerDistribution.Id);
}
/**
* @param {Number} percentage
* @returns {Promise}
*/
_changeCloudFrontCNames(percentage) {
let cloudFrontService = this.replication.cloudFrontService;
let blueDistribution = cloudFrontService.blueConfig();
let greenDistribution = cloudFrontService.greenConfig();
return cloudFrontService.getDistributionCNAMES(blueDistribution.id).then(cNames => {
let resolver = new _CNAMEResolver.CNAMEResolver(cNames);
let domain = resolver.resolveDomain();
let domainCName = `*.${domain}`;
if (cNames.indexOf(domainCName) !== -1) {
throw new _CNAMEAlreadyExistsException.CNAMEAlreadyExistsException(domainCName);
}
return cloudFrontService.getDistributionCNAMES(greenDistribution.id);
}).then(cNames => {
this.parameters = this.buildParameters(percentage, cNames);
let domainCName = `*.${this.parameters.domain}`;
return cloudFrontService.changeCloudFrontCNAMEs(blueDistribution.id, [domainCName]).catch(e => {
if (e.code === 'CNAMEAlreadyExists') {
throw new _CNAMEAlreadyExistsException.CNAMEAlreadyExistsException(domainCName);
}
throw e;
});
});
}
/**
* Refactor this s**tty method
*
* @param {Object} blueOriginalAliases
*
* @returns {Promise}
* @private
*/
_createBlue2ndDNSRecord(blueOriginalAliases) {
let resolver = new _CNAMEResolver.CNAMEResolver(blueOriginalAliases.Items);
let blueOriginalHostname = resolver.resolveHostname();
let blueDomain = resolver.resolveDomain();
let blueSubDomain = blueOriginalHostname.split('.').shift();
let route53Service = this.replication.route53Service;
let blueDistribution = this.replication.cloudFrontService.blueConfig();
return route53Service.findRoute53RecordsByCfCNameDomain(this.parameters.domain, blueDistribution.domain).then(result => {
this._blueRoute53Record = {
HostedZone: result.HostedZone,
RecordSet: this.resolveSuitableRecord(result.Records)
};
let hostedZone = this._blueRoute53Record.HostedZone;
let recordSet = this._blueRoute53Record.RecordSet;
let cNameCounter = 0;
let nextCName = () => `${blueSubDomain}${++cNameCounter}.${blueDomain}`;
let tryCreateNextRecord = () => {
let currentCName = nextCName();
let createAction = new _RecordSetAction.RecordSetAction(recordSet).create().name(currentCName);
if (this.askRecordChangePermissions([createAction])) {
return route53Service.applyRecordSetActions(hostedZone.Id, [createAction]).then(() => {
this.parameters.blueBase = `https://${currentCName}`;
console.debug(`Using "${this.parameters.blueBase}" as blue environment second base`);
}).catch(e => {
if (e.code === 'InvalidChangeBatch') {
console.warn(`"${currentCName}" is already used. Trying another CNAME for second blue record...`);
return tryCreateNextRecord();
}
throw e;
});
}
return Promise.resolve();
};
return tryCreateNextRecord();
});
}
/**
* @param {Object[]} records
* @returns {Object}
*/
resolveSuitableRecord(records) {
if (records.length === 1) {
return records[0];
}
let recordsObj = records.reduce((obj, record) => {
obj[record.Name.slice(0, -1)] = record;
return obj;
}, {});
let hostnameChoice = null;
let prompt = new _Prompt.Prompt(`Multiple Route53 Records found for "${records[0].AliasTarget.DNSName}" cloudfront. ${_os2.default.EOL}` + `Which one you would like to use for blue green deployment? `);
prompt.syncMode = true;
prompt.readChoice(choice => {
hostnameChoice = choice;
}, Object.keys(recordsObj));
return recordsObj[hostnameChoice];
}
/**
* @returns {Promise}
* @private
*/
_createTrafficManagerCfDistribution() {
let cloudFrontService = this.replication.cloudFrontService;
let blueDistribution = cloudFrontService.blueConfig();
return cloudFrontService.cloneDistribution(blueDistribution.id, {
Aliases: { Quantity: 0, Items: [] },
CallerReference: this._trafficManagerDistributionIdentifier
}).then(response => {
let newDistribution = response.Distribution;
return this.attachLambdaEdgeFunction(newDistribution.Id).then(() => newDistribution);
});
}
/**
* @param {Object} cfBalancerDistribution
* @returns {Promise}
* @private
*/
_updateBlueMainDNSRecord(cfBalancerDistribution) {
let route53Service = this.replication.route53Service;
let route53Record = this._blueRoute53Record;
let hostedZone = route53Record.HostedZone;
let recordSet = route53Record.RecordSet;
let updateRecord = new _RecordSetAction.RecordSetAction(recordSet).upsert().aliasTarget({
DNSName: cfBalancerDistribution.DomainName,
HostedZoneId: _CloudFrontService.CloudFrontService.CF_HOSTED_ZONE_ID,
EvaluateTargetHealth: false
});
if (this.askRecordChangePermissions([updateRecord])) {
return route53Service.applyRecordSetActions(hostedZone.Id, [updateRecord]);
}
return Promise.resolve();
}
/**
* @returns {Promise}
*/
_askForBlue2ndDNSRecord() {
// @todo: implement
return Promise.resolve();
}
/**
* @param {String} distributionId
* @returns {Promise}
*/
attachLambdaEdgeFunction(distributionId) {
let cloudFrontService = this.replication.cloudFrontService;
let lambdaService = this.replication.lambdaService;
let functionName = lambdaService.cloudFrontTrafficManagerFunctionName;
let eventType = _CloudFrontEvent.CloudFrontEvent.VIEWER_REQUEST;
return lambdaService.compileLambdaForCloudFront(functionName, this.parameters).then(() => lambdaService.addLambdaEdgeInvokePermission(functionName, distributionId)).then(() => {
console.info(`Attaching "${functionName}" to ${distributionId} ${eventType} event.`);
return cloudFrontService.attachLambdaToDistributionEvent(lambdaService.generateLambdaArn(functionName), distributionId, eventType);
}).then(() => {
console.info(`Function "${functionName} has been attached to ${distributionId} ${eventType} event.`);
});
}
/**
* @param {Object} parameters
*/
set parameters(parameters) {
this._config.parameters = parameters;
}
/**
* @returns {Object}
*/
get parameters() {
return this._config.parameters;
}
/**
* @returns {Object}
*/
get balancerDistribution() {
return this._config.balancerDistribution;
}
/**
* @param {Object} balancerDistribution
*/
set balancerDistribution(balancerDistribution) {
this._config.balancerDistribution = balancerDistribution;
}
/**
* @returns {String}
*/
get _trafficManagerDistributionIdentifier() {
return `deep.blue-green-distribution.${this.replication.hashCode}`;
}
/**
* @todo: inject those variables into publish strategy
*
* @param {Number} percentage
* @param {String[]} cNames
* @returns {Object}
*/
buildParameters(percentage, cNames) {
if (cNames.length === 0) {
throw new _MissingCNAMEException.MissingCNAMEException();
}
let cNameResolver = new _CNAMEResolver.CNAMEResolver(cNames);
let hostname = cNameResolver.resolveHostname();
let domain = cNameResolver.resolveDomain();
let greenBase = `https://${hostname}`;
let blueBase = `https://www1.${domain}`; // @todo: implement a smart subdomain generation for blue environment
console.debug(`Using "${domain}" as application domain`);
console.debug(`Using "${greenBase}" as green environment base`);
return {
percentage,
blueBase,
greenBase,
domain
};
}
}
exports.BalancedStrategy = BalancedStrategy;