hapiest-deploy
Version:
A deployment tool. Initially for AWS ElasticBeanstalk but it may expand in scope over time.
343 lines (308 loc) • 12.3 kB
JavaScript
;
const Promise = require('bluebird');
const parseArgv = require('minimist');
const DeployExecutionServiceFactory = require('./deployExecutionServiceFactory');
class DeployService {
/**
* @name DeployCredentials
* @type {Object}
* @property {Object} awsCredentials
* @property {string} awsCredentials.accessKeyId
* @property {string} awsCredentials.secretAccessKey
*/
/**
* @name DeployConfig
* @type {Object}
* @property {string} region
* @property {string} s3Bucket
* @property {EbApplicationConfig[]} ebApplications
*/
/**
* @name EbApplicationConfig
* @type {Object}
* @property {string} name
* @property {string} ebApplicationName
* @property {EbEnvironmentConfig[]} ebEnvironments
*/
/**
* @name EbEnvironmentConfig
* @type {Object}
* @property {string} name
* @property {string} ebEnvironmentName
* @property {string} ebEnvironmentId
* @property {string} gitBranch
*/
/**
* @name DeployFolders
* @type {Object}
* @property {string} apps
* @property {string} gitRoot
*/
/**
* @name DeployPreHookFunction
* @type {Function}
* @param {DeployExecutionServiceInfo|DeployExecutionServiceInfo[]} info
* @param {DeployRequest|DeployRequestMultiple} deployRequest
* @param {Logger} logger
* @returns {Promise}
*/
/**
* @name DeployRequest
* @type {Object}
* @property {string} appName
* @property {string} envName
* @property {string} [commitHash] - defaults to looking up commit hash in the repository based on gitBranch configuration setting for the appName:envName
*/
/**
* @name DeployRequestMultiple
* @type {Object}
* @property {string[]} appNames
* @property {string} envName
* @property {string} [commitHash] - defaults to looking up commit hash in the repository based on gitBranch configuration setting for the appName:envName
*/
/**
* @name DeployRuntimeConfig
* @type {Object}
* @property [runPreHook=false] - defaults to looking up commit hash in the repository based on gitBranch configuration setting for the appName:envName
*/
/**
* @param {DeployCredentials} credentials
* @param {DeployConfig} config
* @param {DeployFolders} folders
* @param {Logger} logger
* @param {DeployPreHookFunction} [preHookFunction]
*/
constructor(credentials, config, folders, logger, preHookFunction) {
this._credentials = credentials;
this._config = config;
this._folders = folders;
this._logger = logger;
this._preHookFunction = preHookFunction;
}
/**
* Steps:
* 1) Create the deploy execution service
* 2) Confirm it's configured correctly
* 3) Run the preHookFunction if necessary
* 4) Run the deploy
*
* @param {DeployRequest} deployRequest
* @param {DeployRuntimeConfig} runtimeConfig
* @returns {Promise}
*/
deploy(deployRequest, runtimeConfig) {
return Promise.resolve()
.then(() => {
const deployExecService = this._getDeployExecutionService(deployRequest);
return Promise.resolve()
.then(() => deployExecService.confirmSettingsOk())
.then(() => this._executeDeployPreHookIfNecessary(runtimeConfig, deployExecService))
.then(() => deployExecService.deploy())
;
})
}
/**
* 1) Create a DeployExecutionService instance for each request
* 2) Confirm that all of them are configured ok
* 3) Execute the preHookFunction if necessary
* 4) Run the deploys in parallel
*
* @param {DeployRequestMultiple} deployRequest
* @param {DeployRuntimeConfig} runtimeConfig
* @returns {Promise}
*/
deployMultiple(deployRequest, runtimeConfig) {
return Promise.resolve(deployRequest.appNames)
.map(appName => this._getDeployExecutionService({appName:appName, envName:deployRequest.envName, commitHash:deployRequest.commitHash}))
.map(deployExecService => deployExecService.confirmSettingsOk().then(() => deployExecService))
.then(deployExecServices => {
return Promise.resolve()
.then(() => this._confirmAllCommitHashesEqual(deployExecServices))
.then(() => this._executeDeployMultiplePreHookIfNecessary(runtimeConfig, deployExecServices))
.then(() => deployExecServices)
})
.map(deployExecService => deployExecService.deploy());
}
/**
* @param {Array} argv
* -a, --application: the app name in the config file you'd like to deploy; can be comma delimited to deploy multiple apps for the same environment simultaneously
* -e, --environment: the environment for the app that you'd like to deploy
* -c, --commit-hash: optional commit hash to deploy; if gitBranch isn't defined for the app:env then it's required
* -p, --run-pre-hook: flag that must be passed to run deployment pre-hook assuming it's provided in constructor
*
* @returns {Promise}
*/
deployFromCommandLineArguments(argv) {
return Promise.resolve()
.then(() => this._parseCommandLineArguments(argv))
.then(args => this.deployMultiple(args.deployRequest, args.runtimeConfig))
;
}
/**
* @param {DeployRequest} deployRequest
* @returns {DeployExecutionService}
* @private
*/
_getDeployExecutionService(deployRequest) {
const ebApp = this._findApp(deployRequest.appName);
const ebEnv = this._findEnv(ebApp, deployRequest.envName);
const deployExecInfo = this._createDeployExecutionServiceInfo(ebApp, ebEnv, deployRequest.commitHash);
const deployExecService = this._createDeployExecutionService(deployExecInfo);
return deployExecService;
}
/**
* @param appName
* @returns {EbApplicationConfig}
* @private
*/
_findApp(appName) {
const appArr = this._config.ebApplications.filter(app => app.name === appName);
if (appArr.length > 1) {
throw new Error(`Invalid configuration: multiple applications with name ${appName}`);
}
if (appArr.length === 0) {
throw new Error(`Invalid configuration: no applications with name ${appName}`);
}
return appArr[0];
}
/**
* @param {EbApplicationConfig} ebApp
* @param {string} envName
* @returns {EbEnvironmentConfig}
* @private
*/
_findEnv(ebApp, envName) {
const envArr = ebApp.ebEnvironments.filter(env => env.name === envName);
if (envArr.length > 1) {
throw new Error(`Invalid configuration: application ${ebApp.name} has multiple environments with name ${envName}`);
}
if (envArr.length === 0) {
throw new Error(`Invalid configuration: application ${ebApp.name} has no environment named ${envName}`);
}
return envArr[0];
}
/**
* @param {EbApplicationConfig} ebApp
* @param {EbEnvironmentConfig} ebEnv
* @param {string} [commitHash]
* @returns {DeployExecutionServiceInfo}
* @private
*/
_createDeployExecutionServiceInfo(ebApp, ebEnv, commitHash) {
return {
region: this._config.region,
s3Bucket: this._config.s3Bucket,
appName: ebApp.name,
ebApplicationName: ebApp.ebApplicationName,
envName: ebEnv.name,
ebEnvironmentName: ebEnv.ebEnvironmentName,
ebEnvironmentId: ebEnv.ebEnvironmentId,
gitBranch: ebEnv.gitBranch,
commitHash: commitHash
}
}
/**
* @param {DeployExecutionServiceInfo} deployExecInfo
* @returns {DeployExecutionService}
* @private
*/
_createDeployExecutionService(deployExecInfo) {
return DeployExecutionServiceFactory.create(this._credentials, deployExecInfo, this._folders, this._logger);
}
/**
* @param {DeployExecutionService} deployExecServices
* @returns {Promise}
* @private
*/
_confirmAllCommitHashesEqual(deployExecServices) {
return Promise.resolve(deployExecServices)
.map(service => service.getCommitHash())
.reduce((priorCommitHash, commitHash) => {
if (priorCommitHash !== commitHash) {
throw new Error(`All commit hashes must be equal when deploying multiple environments simultaneously (${priorCommitHash} vs ${commitHash})`);
}
return commitHash;
});
}
/**
* @param {DeployRuntimeConfig} runtimeConfig
* @param {DeployExecutionService} deployExecService
* @returns {Promise}
* @private
*/
_executeDeployPreHookIfNecessary(runtimeConfig, deployExecService) {
const info = deployExecService.info;
const commitHash = deployExecService.getCommitHash();
return this._executePreHookIfNecessary(runtimeConfig, info, commitHash);
}
/**
* Note, must have already called _confirmAllCommitHashesEqual prior to this function
*
* @param {DeployRuntimeConfig} runtimeConfig
* @param {DeployExecutionService[]} deployExecServices
* @returns {Promise}
* @private
*/
_executeDeployMultiplePreHookIfNecessary(runtimeConfig, deployExecServices) {
return Promise.resolve()
.then(() => {
const infoArr = deployExecServices.map(service => service.info);
const commitHash = deployExecServices[0].getCommitHash();
return Promise.all([infoArr, commitHash])
})
.spread((infoArr, commitHash) => this._executePreHookIfNecessary(runtimeConfig, infoArr, commitHash))
}
/**
* @param {DeployRuntimeConfig} runtimeConfig
* @param {DeployExecutionServiceInfo|DeployExecutionServiceInfo[]} info
* @param {string} commitHash
* @returns {Promise}
* @private
*/
_executePreHookIfNecessary(runtimeConfig, info, commitHash) {
const preHookFunction = runtimeConfig.runPreHook && this._preHookFunction ? this._preHookFunction : null;
const preHookPromise = preHookFunction ?
Promise.resolve().then(() => preHookFunction(info, commitHash, this._logger)) :
Promise.resolve();
return preHookPromise;
}
/**
* @param {Array} argv
* -a, --application: the app name in the config file you'd like to deploy; can be comma delimited to deploy multiple apps for the same environment simultaneously
* -e, --environment: the environment for the app that you'd like to deploy
* -c, --commit-hash: optional commit hash to deploy; if gitBranch isn't defined for the app:env then it's required
* -p, --run-pre-hook: flag that must be passed to run deployment pre-hook assuming it's provided in constructor
*
* @returns {{deployRequest: DeployRequest, runtimeConfig: DeployRuntimeConfig}}
* @private
*/
_parseCommandLineArguments(argv) {
const argvObj = parseArgv(argv);
const appNamesString = argvObj.a || argvObj.application;
const envName = argvObj.e || argvObj.environment;
const commitHash = argvObj.c || argvObj['commit-hash'];
const runPreHook = !!(argvObj.p || argvObj['run-pre-hook']);
if (!appNamesString) {
throw new Error('Invalid argv - option -a / --application required');
}
if (!envName) {
throw new Error('Invalid argv - option -e / --environment required');
}
if(commitHash && commitHash.length !== 40) {
throw new Error('Invalid argv - option -c / --commit-hash must be a full length git hash of 40 characters');
}
const appNames = appNamesString.split(',');
const returnObj = {
deployRequest: {
appNames: appNames,
envName: envName,
commitHash: commitHash
},
runtimeConfig: {
runPreHook: runPreHook
}
};
return returnObj;
}
}
module.exports = DeployService;