UNPKG

hapiest-deploy

Version:

A deployment tool. Initially for AWS ElasticBeanstalk but it may expand in scope over time.

245 lines (216 loc) 9.04 kB
'use strict'; const Fs = require('fs'); const AdmZip = require('adm-zip'); const Git = require('nodegit'); const Path = require('path'); const Promise = require('bluebird'); class DeployExecutionService { /** * @param {AWS.S3} s3 * @param {AWS.ElasticBeanstalk} eb * @param {DeployExecutionServiceInfo} info * @param {DeployFolders} folders * @param {Logger} logger */ constructor (s3, eb, info, folders, logger) { this._s3 = s3; this._eb = eb; this._info = info; this._folders = folders; this._logger = logger; } /** * @returns {DeployExecutionServiceInfo} */ get info () { return this._info; } confirmSettingsOk () { if (!this._info.commitHash && !this._info.gitBranch) { const err = new Error('Neither Git branch nor commit hash provided. Please specify at least one.'); this._logger.error(err.message, { info: this._info }); return Promise.reject(err); } return Promise.resolve(); } /** * High level execution summary * * 1) Create a zip file (application bundle) containing the necessary EB files (at time of writing this comment, only Dockerrun.aws.json) * 2) Upload the zip file (application bundle) to S3 * 3) Create a new EB application version * 4) Update the EB environment with the new application version * * @returns {Promise} */ deploy () { return Promise.resolve() .then(() => this.confirmSettingsOk()) .tap(() => this._logger.info('Deploy: attempting to deploy application', this._info)) .then(() => { let commitHash; return this.getCommitHash() .then(_commitHash => { commitHash = _commitHash; return this._createEbApplicationBundleBuffer(commitHash); }) .then(appBundleBuffer => this._uploadEbApplicationBundleToS3(commitHash, appBundleBuffer)) }) .then(s3Info => this._createNewEbApplicationVersion(s3Info)) .then(s3Info => this._updateEbEnvironment(s3Info)); } /** * @returns {Promise.<String>} */ getCommitHash () { const commitHashPromise = this._info.commitHash ? Promise.resolve(this._info.commitHash) : this._getCommitHashFromRepo(); return commitHashPromise; } /** * @returns {Promise.<string>} * @private */ _getCommitHashFromRepo () { const gitBranch = this._info.gitBranch; const repoPath = this._folders.gitRoot; return Git.Repository.open(repoPath) .then(repo => repo.getBranchCommit(gitBranch)) .then(commit => commit.sha()); } /** * @returns {Buffer} * @private */ _createEbApplicationBundleBuffer (commitHash) { const zip = new AdmZip(); const localDockerrunPath = Path.join(this._folders.apps, this._info.appName, 'Dockerrun.aws.json'); const dockerrunContents = Fs.readFileSync(localDockerrunPath, 'utf-8').replace('{{TAG}}', `:${commitHash}`); const ebExentensionsPath = this._fetchEbExtensionsFolder(); zip.addFile('Dockerrun.aws.json', new Buffer(dockerrunContents)); if (ebExentensionsPath) zip.addLocalFolder(ebExentensionsPath, '.ebextensions'); return zip.toBuffer(); } /** * @returns {Buffer} * @private */ _fetchEbExtensionsFolder () { const localEbExtensionsFolderPath = Path.join(this._folders.apps, this._info.appName, this._info.ebEnvironmentName, '.ebextensions'); const pathExists = Fs.existsSync(localEbExtensionsFolderPath); return pathExists && localEbExtensionsFolderPath; } /** * @name DeployExecutionServiceS3CreationInfo * @type {Object} * @property {string} s3Key * @property {string} filenameNoExtension */ /** * @param {string} sha * @param {Buffer} zipBuffer * @returns {Promise.<DeployExecutionServiceS3CreationInfo, Error>} * @private */ _uploadEbApplicationBundleToS3 (sha, zipBuffer) { this._logger.info(`Deploy: Commit SHA hash ${sha}`); const s3Info = {}; s3Info.filenameNoExtension = `app-${sha}`; s3Info.s3Key = `${this._info.ebApplicationName}/${s3Info.filenameNoExtension}.zip`; const s3PutObjectParams = { Key: s3Info.s3Key, Body: zipBuffer }; this._logger.info(`Deploy: Attempting to upload application bundle to S3`, { key: s3Info.Key, bucket: this._info.s3Bucket }); return new Promise((resolve, reject) => { this._s3.putObject(s3PutObjectParams, (err, data) => { if (err) { this._logger.error(`Deploy: Failed uploading application bundle to S3`, err); reject(err); } else { this._logger.info(`Deploy: succeeded adding application bundle to S3`, data); resolve(s3Info); } }); }); } /** * @param {DeployExecutionServiceS3CreationInfo} s3Info * @private */ _createNewEbApplicationVersion (s3Info) { const newApplicationVersion = { ApplicationName: this._info.ebApplicationName, Description: '', VersionLabel: s3Info.filenameNoExtension, SourceBundle: { S3Bucket: this._info.s3Bucket, S3Key: s3Info.s3Key }, AutoCreateApplication: false, // Don't create a new EB application! That would be kind of bad Process: true // Check the EB config files (e.g., Dockerrun.aws.json) for errors }; this._logger.info('Deploy: creating new ElasticBeanstalk application version', newApplicationVersion); return new Promise((resolve, reject) => { this._eb.createApplicationVersion(newApplicationVersion, (err, data) => { if (err) { if (err.code === 'InvalidParameterValue' && err.message === `Application Version ${newApplicationVersion.VersionLabel} already exists.`) { this._logger.info(`Deploy: application version ${newApplicationVersion.VersionLabel} already exists`); resolve(s3Info); } else { this._logger.error('Deploy: failed to create new ElasticBeanstalk application version', err); reject(err); } } else { this._logger.info('Deploy: ElasticBeanstalk application version created', data); resolve(s3Info); } }); }) .then(() => this._waitForAppVersionToBeAvailable(s3Info)) } // Creating a function for this so we can mock it out in tests and avoid a 10s delay _waitForAppVersionToBeAvailable (s3Info) { this._logger.info('Deploy: sleeping for 10s to allow application version to finalize before updating environment'); return new Promise((resolve, reject) => { setTimeout(() => { resolve(s3Info) }, 10000); }); // @TODO: add recursive checks for application version readiness //const describeAppVersions = { // ApplicationName: config.applications.web.applicationName, // VersionLabels: [fileInfo.filenameNoExtension] //}; //beanstalk.describeApplicationVersions() } /** * @param {DeployExecutionServiceS3CreationInfo} s3Info * @returns {Promise} * @private */ _updateEbEnvironment (s3Info) { const environmentUpdate = { ApplicationName: this._info.ebApplicationName, EnvironmentId: this._info.ebEnvironmentId, EnvironmentName: this._info.ebEnvironmentName, VersionLabel: s3Info.filenameNoExtension }; this._logger.info('Deployment: attempting to update ElasticBeanstalk environment', environmentUpdate); return new Promise((resolve, reject) => { this._eb.updateEnvironment(environmentUpdate, (err, data) => { if (err) { this._logger.error('Deployment: failed on updating ElasticBeanstalk environment', err); reject(err); } else { this._logger.info('Deployment: ElasticBeanstalk environment update API call succeeded. Check ElasticBeanstalk Dashboard to monitor deploy progress.', data); this._logger.info(`https://console.aws.amazon.com/elasticbeanstalk/home?region=${this._info.region}#/environment/dashboard?applicationName=${this._info.ebApplicationName}&environmentId=${this._info.ebEnvironmentId}`) resolve(data); } }); }) } } module.exports = DeployExecutionService;