briareus
Version:
Briareus assists with Feature Branch deploys to ECS
332 lines (292 loc) • 9.06 kB
JavaScript
const _ = require('lodash');
const async = require('async');
const jsonpatch = require('fast-json-patch');
const uuidv4 = require('uuid/v4');
const Monitor = require('ecs-deployment-monitor');
const renderStates = require('../../node_modules/ecs-deployment-monitor/lib/renderer/states');
const utils = require('../utilities');
const persistance = require('./persistance');
const recon = require('./recon');
const Pipeline = require('./pipeline');
const errors = require('./errors');
class Variant {
constructor(ctx, data) {
this.ctx = ctx;
if (!data.partitionKey) {
// If this is a new variant; initialize
let hashedSlug = utils.hashSlug(data.slug);
let host = `${hashedSlug}.${data.baseHostname}`;
_.defaults(data, {
partitionKey: `variant-${hashedSlug}`,
sortKey: 'variant',
id: hashedSlug,
hashedSlug: hashedSlug,
name: `briareus-${hashedSlug}`,
endpoint: {
host,
url: `https://${host}`
},
createdAt: Math.floor(new Date() / 1000),
events: [],
deployments: [],
errors: [],
provisioned: false,
assets: {
dnsRecords: [],
taskDefinitions: [],
ssmParameters: {}
},
ssmParameterScopePrefix: `/briareus/${hashedSlug}`,
state: 'created',
});
}
this.data = data;
this.ctx.logData.slug = this.data.slug;
this.ctx.logData.variantHashedSlug = this.data.hashedSlug;
}
/**
* setState
*
* Change the state this deployment is in
*
* @param {string} state the state to set
* @return {boolean}
*/
setState(state) {
this.ctx.log.info(`Setting Variant State to "${state}"`);
this.data.state = state;
this.emit('state', { state });
}
emit(event, data = {}) {
let decoratedEvent = {
id: uuidv4(),
createdAt: Math.floor(new Date()),
name: event,
data
};
this.ctx.log.info({ event: decoratedEvent }, `Variant Event`);
this.data.events.push(decoratedEvent);
}
recon(cb) {
this.ctx.log.info(`Starting Recon`);
let variantAware = _.map(_.keys(recon), (reconName) => {
let fn = recon[reconName];
return (done) => {
this.ctx.log.info(`Running Recon: ${reconName}`);
fn(this.data, done);
}
});
async.parallel(variantAware, (err, results) => {
if (err) return cb(err);
this.data = jsonpatch.applyPatch(this.data, _.flatten(results)).newDocument;
cb();
});
}
_exec(options, cb) {
this.emit('exec:start');
this.setState(options.stateStart);
async.series([
(next) => this.save(next),
(next) => options.pipeline.run(next)
], (err) => {
if (err) this.setState('error');
else this.setState(options.stateEnd);
// Complete next tick so 'exec:end' event doesn't
// occur at the same time as the pipeline end events
process.nextTick(() => {
this.emit('exec:end');
this.save((err2) => cb(err))
});
});
}
provision(cb) {
this.recon((err) => {
if (err) return cb(err);
const phases = [
{
id: 'creating_infrastructure',
description: 'Creating Build Infrastructure',
actions: [
'CreateAcmCertificate',
'CreateDnsRecordsCertificateVerification',
'WaitForAcmCertificateVerification',
'AddAcmCertificateToAlbListener',
'CreatePlaceholderEcsTaskDefinition',
'CreateAlbTargetGroup',
'SetAlbTargetGroupAttributes',
'CreateAlbRule',
'CreateDnsRecordsEndpoint',
'CreateEcsService',
]
}
];
const options = {
stateStart: 'provisioning',
stateEnd: 'provisioned',
pipeline: new Pipeline(this.ctx, this.data, phases)
}
this._exec(options, (err) => {
if (err) return cb(err);
this.data.provisioned = true;
this.save(cb)
});
})
}
destroy(cb) {
const phases = [{
id: 'DestroyingInfrastructure',
description: 'Destroying Infrastructure',
actions: [
'DestroyDnsRecords',
'RemoveAcmCertificateFromAlbListener',
'DestroyAcmCertificate',
'ScaleDownEcsService',
'DestroyEcsService',
'DestroyAlbRule',
'DestroyAlbTargetGroup',
'DeregisterTaskDefinitions',
'DestroySsmParameters'
]
}];
const params = {
stateStart: 'destroying',
stateEnd: 'destroyed',
pipeline: new Pipeline(this.ctx, this.data, phases)
}
this._exec(params, (err) => {
if (err) return cb(err);
this.archive(cb);
});
}
deploy(options, cb) {
_.assign(this.data, _.pick(options, ['image', 'envs', 'secrets', 'taskDefinition', 'gitCommit', 'buildUrl', 'pullRequestId']));
this.data.lastDeploymentAt = Math.floor(new Date() / 1000);
this.emit('deploy:start');
const phases = [
{
id: 'CreatingRelease',
description: 'Creating Build Release',
actions: [
'SyncSsmParameterSecrets',
'CreateBuildEcsTaskDefinition',
'DeployBuildTaskDefinition',
]
}
];
const params = {
stateStart: 'deploying',
stateEnd: 'deployed',
pipeline: new Pipeline(this.ctx, this.data, phases)
}
async.series([
(next) => this.save(next),
(next) => this._exec(params, next),
(next) => this.monitor(this.data.activeDeployment.taskDefinition.arn, next),
// Set a timeout to allow existing dynamodb write to finish from monitor
(next) => setTimeout(next, 2000)
], (err) => {
let data = {};
if (err) {
data.error = err.message;
}
// Always fire an end event
this.emit('deploy:end', data);
this.save(() => {
if (err) return cb(err);
return cb(null, _.last(this.data.deployments));
});
});
}
/**
* monitor
*
* Shoehorn ECS Deployment Monitor into Briareus. Translate state change events
* into pipeline events that the CLI renderer understands
*
* @param {string} taskDefinitionArn The task definition to monitor
* @param {function} cb The callback
* @return {boolean}
*/
monitor(taskDefinitionArn, cb) {
let hasError = false;
let pipeline = new Pipeline(this.ctx, this.data, []);
const monitor = Monitor({
serviceName: this.data.name,
clusterArn: this.data.ecsClusterArn,
taskDefinitionArn: taskDefinitionArn
});
const phaseEventMetadata = {
phase: {
id: 'MonitorDeploy'
}
};
pipeline.startStep('phase', phaseEventMetadata, { message: 'Monitor deployment of Build Release' });
monitor.on('error', (err) => {
hasError = true;
event = {
action: {
id: monitor.state,
},
monitor: {
isSteady: monitor.isSteady(),
isFailure: monitor.isFailure(),
}
}
this.data.errors.push(err);
pipeline.errorStep('action', event, { message: err.message }, (err2) => cb(err));
});
monitor.on('state', (state) => {
// Process on next tick so that action:complete's from previous states are
// processed before action:start
process.nextTick(() => {
let stateInfo = renderStates[state](monitor);
stateInfo.id = state;
let event = {
action: _.pick(stateInfo, ['id']),
monitor: {
isSteady: monitor.isSteady(),
isFailure: monitor.isFailure(),
}
};
if (monitor.isFailure()) {
hasError = true;
pipeline.errorStep('action', event, {
message: stateInfo.done,
extra: stateInfo.extra
}, (err) => {
cb(new errors.DeploymentFailure(stateInfo.done));
});
}
else {
pipeline.startStep('action', event, { message: stateInfo.waiting });
if (state === 'Steady') pipeline.completeStep('action', event, { message: stateInfo.done })
else monitor.once('state', () => {
pipeline.completeStep('action', event, { message: stateInfo.done })
});
this.save(_.noop);
}
});
});
monitor.on('end', () => {
// Process on next tick so that steady's action:complete is processed
// before we call the callback
setTimeout(() => {
// If we have an upstream error which has already called
// the callback then return. Nothing to do
if (hasError) return;
pipeline.completeStep('phase', phaseEventMetadata, { message: "Phase Complete" });
cb(null, []);
}, 1000);
});
}
save(cb) {
this.ctx.log.info(`Saving Variant`);
persistance.putItem(this.data, cb)
}
archive(cb) {
this.ctx.log.info(`Archiving Variant`);
persistance.archiveVariant(this.data, cb);
}
}
module.exports = Variant;