UNPKG

briareus

Version:

Briareus assists with Feature Branch deploys to ECS

332 lines (292 loc) 9.06 kB
'use strict' 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;