deepify
Version:
DEEP Development Tools
546 lines (432 loc) • 15 kB
JavaScript
/**
* Created by AlexanderC on 6/19/15.
*/
/*eslint max-statements: 0, no-unused-expressions: 0*/
;
module.exports = function(mainPath) {
let path = require('path');
let fs = require('fs');
let fse = require('fs-extra');
let os = require('os');
let Exec = require('../../lib.compiled/Helpers/Exec').Exec;
let Bin = require('../../lib.compiled/NodeJS/Bin').Bin;
let Prompt = require('../../lib.compiled/Terminal/Prompt').Prompt;
let LambdasExtractor = require('../../lib.compiled/Helpers/LambdasExtractor').LambdasExtractor;
let Property = require('deep-package-manager').Property_Instance;
let SharedAwsConfig = require('deep-package-manager').Helpers_SharedAwsConfig;
let Config = require('deep-package-manager').Property_Config;
let S3Service = require('deep-package-manager').Provisioning_Service_S3Service;
let ProvisioningCollisionsDetectedException = require('deep-package-manager')
.Property_Exception_ProvisioningCollisionsDetectedException;
let DeployConfig = require('deep-package-manager').Property_DeployConfig;
let Listing = require('deep-package-manager').Provisioning_Listing;
let resourcesToUpdate = this.opts.locate('action').value;
let isProd = this.opts.locate('prod').exists;
let localOnly = this.opts.locate('dry-run').exists;
let dumpCodePath = this.opts.locate('dump-local').value;
let cfgBucket = this.opts.locate('cfg-bucket').value;
let appEnv = isProd ? 'prod' : this.opts.locate('env').value;
let microservicesToDeploy = this.opts.locate('partial').value;
let frontendOnly = this.opts.locate('frontend').exists ? Property.DEPLOY_FRONTEND : 0;
let backendOnly = this.opts.locate('backend').exists ? Property.DEPLOY_BACKEND : 0;
let debugBuild = this.opts.locate('debug-build').exists;
let validateNodeVersion = require('./helper/validate-node-version');
let undeployRunning = false;
validateNodeVersion.call(this);
mainPath = this.normalizeInputPath(mainPath);
if (dumpCodePath) {
dumpCodePath = this.normalizeInputPath(dumpCodePath);
}
let propertyInstance = Property.create(mainPath);
let configFile = path.join(propertyInstance.path, Config.DEFAULT_FILENAME);
let config = propertyInstance.config;
if (frontendOnly || backendOnly) {
propertyInstance.deployFlags = frontendOnly | backendOnly;
}
appEnv = appEnv ? appEnv.toLowerCase() : null;
if (appEnv && DeployConfig.AVAILABLE_ENV.indexOf(appEnv) === -1) {
console.error(`Invalid environment ${appEnv}. Available environments: ${DeployConfig.AVAILABLE_ENV.join(', ')}`);
this.exit(1);
}
if (localOnly) {
console.debug('Local mode on!');
}
if (appEnv) {
config.env = appEnv;
}
let arrayUnique = (a) => {
return a.reduce((p, c) => {
if (p.indexOf(c) < 0) {
p.push(c);
}
return p;
}, []);
};
let dumpConfig = (propertyInstance, cb) => propertyInstance.configObj.completeDump(cb);
let getCfDomain = propertyInstance => `http://${propertyInstance.config.provisioning.cloudfront.domain}`;
let ensureAWSProdKeys = (cb) => {
(new SharedAwsConfig()).refillPropertyConfigIfNeeded(config, (refilled) => {
if (refilled) {
fse.outputJsonSync(configFile, config);
}
cb();
});
};
let hasDeployedResources = cb => {
let lister = new Listing(propertyInstance);
lister.list(listingResult => {
cb(listingResult.matchedResources > 0);
});
};
let deployRollback = cb => {
if (propertyInstance.isUpdate || undeployRunning) {
return cb(null); // @todo: undeploy either the update?
}
undeployRunning = true;
hasDeployedResources(has => {
if (!has) {
return cb(null);
}
let prompt = new Prompt('Do you want to undeploy deployed resources?');
prompt.readConfirm(result => {
if (!result) {
return cb(null);
}
let baseHash = propertyInstance.configObj.baseHash;
let env = propertyInstance.config.env;
console.log(`Start undeploying resources for ${baseHash}/${env}`);
// generate a fake resource name to use both env and baseHash on undeploy
let fakeResourceName = `deep.${env}.public.${baseHash}`;
let undeployCmd = new Exec(
Bin.node,
this.scriptPath,
'undeploy',
propertyInstance.path,
`--resource=${fakeResourceName}`
);
if (isProd) {
undeployCmd.addArg('--prod');
}
undeployCmd.run(() => {
if (undeployCmd.failed) {
return cb(undeployCmd.error);
}
cb(null);
}, true);
});
});
};
let getLambdas = (dir, files_) => {
files_ = files_ || [];
let files = fs.readdirSync(dir);
for (let i = 0; i < files.length; i++) {
let file = path.join(dir, files[i]);
if (/\.zip$/i.test(file)) {
files_.push(file);
}
}
return files_;
};
let dumpLambdas = () => {
let lambdas = getLambdas(propertyInstance.path);
if (lambdas.length <= 0) {
console.debug('There are no Lambdas to be dumped!');
return;
}
fse.ensureDirSync(dumpCodePath);
let globalAwsConfigFile = path.join(dumpCodePath, '.aws.json');
fs.outputJsonSync(globalAwsConfigFile, config.aws);
let lambdasVector = [];
let stack = lambdas.length;
for (let i = 0; i < lambdas.length; i++) {
let lambdaPath = lambdas[i];
let newLambdaPath = path.join(
dumpCodePath,
path.basename(lambdaPath, '.zip').replace(/^lambda_/i, '')
);
let awsConfigFile = path.join(newLambdaPath, '.aws.json');
lambdasVector.push(path.basename(newLambdaPath));
console.debug('Unpacking Lambda into ' + newLambdaPath);
// @todo: find a smarter way to deny lambda runtime installing deps in runtime
try {
fs.unlinkSync(path.join(lambdaPath, 'package.json'));
} catch (e) {
console.error('Failed to unlinkSync: ', e);
}
new Exec(
'unzip',
'-qq',
'-o',
lambdaPath,
'-d',
newLambdaPath
)
.avoidBufferOverflow()
.run(function(awsConfigFile, result) {
if (result.failed) {
console.error(result.error);
}
fse.copySync(globalAwsConfigFile, awsConfigFile);
stack--;
console.debug(`Remaining ${stack} Lambdas to be unpacked...`);
}.bind(this, awsConfigFile));
}
let waitUntilDone = () => {
if (stack > 0) {
setTimeout(waitUntilDone, 50);
} else {
fs.unlinkSync(globalAwsConfigFile);
console.debug(`[${lambdasVector.join(', ')}]`);
console.debug('All Lambdas are now ready to run locally!');
}
};
waitUntilDone();
};
let dumpCode = () => {
if (!dumpCodePath) {
return;
}
let tmpFrontendPath = path.join(propertyInstance.path, '_public');
let frontendDumpPath = path.join(dumpCodePath, '_www');
fse.ensureDirSync(frontendDumpPath);
new Exec(
'cp',
'-R',
path.join(tmpFrontendPath, '*'),
frontendDumpPath + '/'
)
.avoidBufferOverflow()
.run((result) => {
if (result.failed) {
console.error(`Unable to dump _frontend code into _www: ${result.error}`);
}
dumpLambdas();
});
};
let getPublicWebsite = propertyInstance => {
return `http://${propertyInstance.config.provisioning.s3.buckets[S3Service.PUBLIC_BUCKET].website}`;
};
let getMicroservicesToDeploy = () => {
if (!microservicesToDeploy) {
return [];
}
let msIdentifiers = arrayUnique(microservicesToDeploy.split(',').map(id => id.trim()));
return typeof msIdentifiers === 'string' ? [msIdentifiers] : msIdentifiers;
};
let getResourcesToUpdate = () => {
if (!resourcesToUpdate) {
return null;
}
let msIdentifiers = arrayUnique(resourcesToUpdate.split(',').map(id => id.trim()));
return typeof msIdentifiers === 'string' ? [msIdentifiers] : msIdentifiers;
};
let doDeploy = () => {
propertyInstance.localDeploy = localOnly;
// @todo: improve it!
// Gracefully teardown...
(() => {
process.on('uncaughtException', (error) => {
if (error instanceof ProvisioningCollisionsDetectedException) {
console.error(
os.EOL,
os.EOL,
'Seems like there are some resources on AWS that may generate collisions while provisioning the web app!',
os.EOL,
`Remove them by running "deepify undeploy ${propertyInstance.path} --resource ${error.collisionHash}"`,
os.EOL,
os.EOL,
error.stringifiedResourcesObj
);
this.exit(1);
} else {
console.error(error.toString(), os.EOL, error.stack);
}
deployRollback((error) => {
if (error) {
console.error(`Error while undeploying resources for ${propertyInstance.configObj.baseHash}: ${error}`);
}
if (propertyInstance.config.provisioning) {
dumpConfig(propertyInstance, () => {
this.exit(1);
});
} else {
this.exit(1);
}
});
});
process.on('SIGINT', () => {
console.debug('Gracefully shutting down from SIGINT (Ctrl-C)...');
deployRollback((error) => {
if (error) {
console.error(`Error while undeploying resources for ${propertyInstance.configObj.baseHash}: ${error}`);
}
if (propertyInstance.config.provisioning) {
dumpConfig(propertyInstance, () => {
this.exit(1);
});
} else {
this.exit(1);
}
});
});
})();
propertyInstance.configObj.tryLoadConfig((error) => {
if (error) {
console.warn(error.message);
this.exit(1);
return;
}
if (propertyInstance.configObj.configExists) {
propertyInstance.update(() => {
console.info(`CloudFront (CDN) domain: ${getCfDomain(propertyInstance)}`);
console.info(`Website address: ${getPublicWebsite(propertyInstance)}`);
dumpConfig(propertyInstance, dumpCode);
}, null, getMicroservicesToDeploy());
} else {
if (microservicesToDeploy) {
console.warn('Partial deploy option is useless during first deploy...');
}
propertyInstance.install(() => {
console.info(`CloudFront (CDN) domain: ${getCfDomain(propertyInstance)}`);
console.info(`Website address: ${getPublicWebsite(propertyInstance)}`);
dumpConfig(propertyInstance, dumpCode);
});
}
}, cfgBucket);
};
let doCompileProd = (propertyPath, cb) => {
console.debug('Start preparing for production');
let cmd = new Exec(
Bin.node,
this.scriptPath,
'compile',
'prod',
propertyPath
);
let resourcesToCompile = microservicesToDeploy || resourcesToUpdate;
resourcesToCompile && cmd.addArg(`--partial="${resourcesToCompile}"`);
if (debugBuild) {
cmd.addArg('--debug-build');
}
cmd.run((result) => {
if (result.failed) {
console.error(`Backend production preparations failed: ${result.error}`);
this.exit(1);
}
cb();
}, true);
};
let prepareProduction = (propertyPath, cb) => {
if (isProd) {
doCompileProd(propertyPath, cb);
} else if (!localOnly && !frontendOnly) {
let prompt = new Prompt('Prepare for production?');
prompt.readConfirm((result) => {
if (result) {
doCompileProd(propertyPath, cb);
return;
}
console.debug('Skipping production preparation...');
cb();
});
} else {
cb();
return;
}
};
let startDeploy = () => {
ensureAWSProdKeys(() => {
propertyInstance.assureFrontendEngine((error) => {
if (error) {
console.error('Error while assuring frontend engine: ' + error);
}
propertyInstance.runInitMsHooks(() => {
propertyInstance.runPreDeployMsHooks(() => {
prepareProduction(propertyInstance.path, doDeploy);
});
});
});
});
};
let removePackedLambdas = () => {
let lambdas = getLambdas(propertyInstance.path);
if (lambdas.length <= 0) {
return;
}
for (let i = 0; i < lambdas.length; i++) {
try {
fs.unlinkSync(lambdas[i]);
} catch (e) {
console.error('Failed to unlinkSync: ', e);
}
}
};
let askForProductionPrepare = (cb) => {
if (debugBuild) {
// if --debug-build is present,
// run 'compile prod' with --debug-build flag, without asking
return cb(true);
}
let prompt = new Prompt(`Prepare for production "${resourcesToUpdate}"?`);
prompt.readConfirm(cb);
};
let updateResources = (resourcesIdentifiers) => {
propertyInstance.configObj.tryLoadConfig((error) => {
if (error) {
console.warn(error.message);
this.exit(1);
return;
}
if (!propertyInstance.configObj.configExists) {
throw new Error('Action deploy is available only on application update');
}
let prepareResources = (path, cb) => {
console.debug(`Skipping "${resourcesToUpdate}" production preparation...`);
cb();
};
askForProductionPrepare((result) => {
if (result) {
prepareResources = doCompileProd.bind(this);
}
prepareResources(propertyInstance.path, () => {
Promise.all(
resourcesIdentifiers.map(resource => {
return new Promise((resolve, reject) => {
try {
propertyInstance.deployAction('@' + resource, () => {
console.info(`"${resource}" has been updated.`);
resolve();
});
} catch(e) {
reject(e);
}
});
})
).then(() => {
removePackedLambdas();
console.info('All resources have been updated.');
}).catch(e => {
setImmediate(() => {
throw e;
});
});
});
});
});
};
if (resourcesToUpdate) {
let resourcesIdentifiers = Object.keys(
new LambdasExtractor(propertyInstance, getResourcesToUpdate())
.extract(LambdasExtractor.NPM_PACKAGE_FILTER, LambdasExtractor.EXTRACT_OBJECT)
);
return updateResources(resourcesIdentifiers);
}
process.on('exit', () => {
new Exec('rm', '-rf', path.join(propertyInstance.path, '_public'))
.avoidBufferOverflow()
.runSync();
removePackedLambdas();
});
startDeploy();
};