UNPKG

mira

Version:

NearForm Accelerator for Cloud Native Serverless AWS

570 lines 24.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MiraBootstrap = void 0; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const chalk_1 = __importDefault(require("chalk")); const child_process_1 = require("child_process"); const app_1 = require("./app"); const mira_config_1 = require("../config/mira-config"); const make_default_config_1 = __importDefault(require("./constructs/config/make-default-config")); const assume_role_1 = require("../assume-role"); const yargs_1 = __importDefault(require("yargs")); const transpiler_1 = __importDefault(require("../transpiler")); const JsonValidation = __importStar(require("../jsonvalidator")); const config_1 = __importDefault(require("config")); const aws_sdk_1 = __importDefault(require("aws-sdk")); const change_detector_1 = __importDefault(require("../change-detector")); const error_logger_1 = __importDefault(require("../error-logger")); const deploy_buckets_1 = require("./deploy-buckets"); const version_1 = __importDefault(require("../version")); const supportedCdkArgs = ['outputs-file', 'require-approval']; /** * @class Responsible for beaming up bits to AWS. Teleportation device not * included. */ class MiraBootstrap { constructor() { this.cdkCommand = path_1.default.join(require.resolve('aws-cdk'), '..', '..', 'bin', 'cdk'); this.docsifyCommand = path_1.default.join(require.resolve('docsify-cli'), '..', '..', 'bin', 'docsify'); this.app = new app_1.MiraApp(); this.errorLogger = new error_logger_1.default(); this.spawn = child_process_1.spawn; } /** * Orchestration used for deployment of the given application. This is used whenever developer or CI will try to * deploy application. It is important to keep this function as a single place for application deployment so, development environment * will have the same deployment process as CI owned environments. * * @param undeploy */ async deploy(undeploy = false) { const envConfig = mira_config_1.MiraConfig.getEnvironment(this.env); this.env = envConfig.name; if (this.args.role) { try { await assume_role_1.assumeRole(this.args.role); } catch (error) { console.warn(chalk_1.default.red('Error Assuming Role'), error.message); return; } } let cmd = 'deploy'; if (undeploy) { cmd = 'destroy'; } const additionalArgs = []; supportedCdkArgs.forEach(attribute => { if (Object.keys(this.args).includes(attribute)) { additionalArgs.push(`--${attribute}=${this.args[attribute]}`); delete this.args[attribute]; } }); if (Object.keys(this.args).includes('stackName')) { additionalArgs.push(this.args.stackName); delete this.args.stackName; } if (Object.prototype.hasOwnProperty.call(this.args, 'dry-run') || Object.prototype.hasOwnProperty.call(this.args, 's3-only')) { deploy_buckets_1.removeAssetDirectories(); cmd = 'synth'; } const commandOptions = [ this.cdkCommand, cmd, '--app', this.getCDKArgs('app.js'), envConfig.name ? `--env=${envConfig.name}` : '', this.env ? `--profile=${this.getProfile(this.env)}` : '', ...additionalArgs ]; const proc = this.spawn('node', commandOptions, { stdio: 'inherit', env: { NODE_ENV: 'dev', ...process.env } }); await new Promise((resolve, reject) => { proc.on('exit', async (code) => { if (code !== 0) { console.error('Deploy Stack Failed. Dumping Error message'); await this.printExtractedNestedStackErrors(); reject(code); return; } resolve(); }); }); if (Object.prototype.hasOwnProperty.call(this.args, 's3-only')) { await deploy_buckets_1.quickDeploy(); } } /** * Orchestration for `npx mira cicd` command. As an effect, CodePipeline and related services will be deployed together * with permission stacks deployed to the target accounts. */ async deployCi() { const permissionFilePath = mira_config_1.MiraConfig.getPermissionsFilePath(); if (!permissionFilePath) { console.error('Permissions file path must be specified either in cicd config or as --file parameter.'); throw new Error('Permissions file path must be specified either in cicd config or as --file parameter.'); } const ciTargetAccounts = mira_config_1.MiraConfig.getCICDAccounts(); let cmd = 'deploy'; if (Object.prototype.hasOwnProperty.call(this.args, 'dry-run')) { cmd = 'synth'; } for (const account of ciTargetAccounts) { const commandOptions = [ this.cdkCommand + (process.platform === 'win32' ? '.cmd' : ''), cmd, '--app', this.getCDKArgs('app.js', true, account.name), account.name ? `--profile=${this.getProfile(account.name)}` : '' ]; console.log(chalk_1.default.cyan(`Starting deployment of CI ${account.name} permissions to Account: ${account.env.account} in ${account.env.region} with profile ${account.profile}.`)); const proc = this.spawn('node', commandOptions, { stdio: 'inherit', env: { ...process.env, NODE_ENV: account.name } }); await new Promise((resolve, reject) => { proc.on('exit', err => { if (err) { console.error(`Deploy CI Permission failed for account '${account.name}'`); reject(err); return; } console.log(chalk_1.default.green(`Done deploying CI ${account.name} permissions.`)); resolve(); }); }); } console.log(chalk_1.default.cyan(`Starting deployment of CI pipeline to Account: ${mira_config_1.MiraConfig.getCICDConfig().account.env.account} in ${mira_config_1.MiraConfig.getCICDConfig().account.env.region} with profile ${mira_config_1.MiraConfig.getCICDConfig().account.profile}.`)); const commandOptions = [ this.cdkCommand + (process.platform === 'win32' ? '.cmd' : ''), cmd, '--app', this.getCDKArgs('ci-app.js'), `--profile=${this.getProfile('cicd')}` ]; const proc = this.spawn('node', commandOptions, { stdio: 'inherit', env: { ...process.env } }); await new Promise((resolve, reject) => { proc.on('exit', err => { if (err) { console.error('Deploy CI pipeline failed'); reject(err); return; } console.log(chalk_1.default.green('Done deploying CI pipeline.')); resolve(); }); }); } /** * Runs docsify web server with the mira docs. */ async runDocs() { const commandOptions = [ this.docsifyCommand + (process.platform === 'win32' ? '.cmd' : ''), 'serve', path_1.default.join(__dirname, '..', '..', '..', 'docs') ]; const proc = this.spawn('node', commandOptions, { stdio: 'inherit', env: { ...process.env } }); await new Promise((resolve, reject) => { proc.on('exit', err => { if (err) { reject(err); return; } console.log(chalk_1.default.green('Done building docs.')); resolve(); }); }); } /** * TODO: check this functionality together with sample app that supports custom domain. */ async deployDomain() { console.log('deploying domain'); const envConfig = mira_config_1.MiraConfig.getEnvironment(this.env); let cmd = 'deploy'; if (Object.prototype.hasOwnProperty.call(this.args, 'dry-run')) { cmd = 'synth'; } const commandOptions = [ this.cdkCommand + (process.platform === 'win32' ? '.cmd' : ''), cmd, '--app', this.getCDKArgs('domain.js'), `--env=${envConfig.name}`, `--profile=${this.getProfile(this.env)}` ]; const proc = this.spawn('node', commandOptions, { stdio: 'inherit', env: { ...process.env } }); await new Promise((resolve, reject) => { proc.on('exit', err => { if (err) { reject(err); return; } console.log(chalk_1.default.green('Done deploying domain.')); resolve(); }); }); } /** * Gets the arguments parsed by the app file provided for the CDK CLI. * @param filename - main application file. * @param isCi - when "npx mira cicd" command is executed permissions file path is taken from the config.file, NOT from the CLI param. * @param env - name of current target environment where the stack with role is going to be deployed. */ getCDKArgs(filename, isCi = false, env) { const resultedEnv = this.env || env; const q = process.platform === 'win32' ? '"' : "'"; let appPath = path_1.default.resolve(__dirname, filename); if (fs_1.default.existsSync('node_modules/mira')) { if (fs_1.default.lstatSync('node_modules/mira/dist').isSymbolicLink()) { // Mira has been locally linked. try { fs_1.default.mkdirSync('node_modules/mira-bootstrap'); } catch (e) { // NOOP } fs_1.default.writeFileSync(`node_modules/mira-bootstrap/bootstrap-${filename}`, `require('mira/dist/src/cdk/${filename}')`, 'utf8'); appPath = `node_modules/mira-bootstrap/bootstrap-${filename}`; } } let appArg = `${q}node --preserve-symlinks "${appPath}" `; // Still inside the quotes, explode the args. // appArg += this.getArgs().join(' ') appArg += !isCi && this.stackFile ? ` --file=${this.stackFile}` : ''; appArg += resultedEnv ? ` --env=${resultedEnv}` : ''; appArg += isCi ? ` --file=${mira_config_1.MiraConfig.getPermissionsFilePath()}` : ''; appArg += q; // End quote. return appArg; } /** * Gets the arguments for this stack. It has built-in support for osx/win support. */ getArgs() { var _a; // eslint-disable-next-line no-useless-rename, @typescript-eslint/no-unused-vars const { _: _, ...args } = this.args; const newArgs = []; for (const key of Object.keys(args)) { if (((_a = args[key]) === null || _a === void 0 ? void 0 : _a.includes) && args[key].includes(' ')) { const q = process.platform !== 'win32' ? "'" : '"'; args[key] = q + args[key] + q; } else if (typeof args[key] === 'boolean') { newArgs.push(`--${key}`); continue; } newArgs.push(`--${key}`, args[key]); } return newArgs; } /** * Gets the profile given the env. */ getProfile(environment) { // if we are in Codebuild environment, return 'client' which is the one set by assume-role if (process.env.CODEBUILD_CI) { return 'client'; } const envConfig = mira_config_1.MiraConfig.getEnvironment(environment); if (envConfig) { return envConfig.profile; } if (this.profile) { return this.profile; } } /** * Verifies wether files provided in the CLI exists. */ async areStackFilesValid() { let isValid = true; for (const stackName of [this.stackFile]) { if (!(await this.app.getStack(stackName))) { console.warn(chalk_1.default.yellow('Stack Not Found:'), stackName); isValid = false; } } return isValid; } /** * Function being called when CLI is invoked. */ async initialize() { version_1.default.checkApplicationCDKVersion(); this.args = this.showHelp(); this.env = this.args.env; this.profile = this.args.profile; if (Object.keys(this.args).includes('env')) { delete this.args.env; } const rawConfig = config_1.default.util.toObject(); const cmd = this.args._[0]; const cd = new change_detector_1.default(process.cwd()); const filesChanged = await cd.run(); let transpiledStackFile; switch (cmd) { case 'domain': await this.deployDomain(); break; case 'init': await make_default_config_1.default(); break; case 'deploy': if (!this.args.file) { console.warn(chalk_1.default.red('Error Initializing'), 'Must supply' + ' a --file=<stackFile> argument.'); return; } if (!JsonValidation.validateConfig(rawConfig)) { console.warn(chalk_1.default.red('Error Initializing'), 'Invalid config file.'); return; } this.stackFile = this.args.file; // Check for file changes if (filesChanged || this.args.force) { if (!this.args.file) { console.warn(chalk_1.default.red('Error Initializing'), 'Must supply' + ' a --file=<stackFile> argument.'); return; } transpiledStackFile = await this.transpile(); if (transpiledStackFile) { this.stackFile = transpiledStackFile; // take a new snapshot because file changed. await cd.takeSnapshot(cd.defaultSnapshotFilePath); } if (await this.areStackFilesValid()) { console.info(chalk_1.default.cyan('Deploying Stack:'), `(via ${chalk_1.default.grey(this.stackFile)})`); await this.deploy(); } } else { console.info(chalk_1.default.yellow('No file changed after last deploy. Skipping.')); } break; case 'undeploy': if (!this.args.file) { console.warn(chalk_1.default.red('Error Initializing'), 'Must supply' + ' a --file=<stackFile> argument.'); return; } this.stackFile = this.args.file; transpiledStackFile = await this.transpile(); if (transpiledStackFile) { this.stackFile = transpiledStackFile; } if (await this.areStackFilesValid()) { console.info(chalk_1.default.cyan('Undeploying Stack:'), `(via ${chalk_1.default.grey(this.stackFile)})`); await this.undeploy(); } else { console.info('If you want to undeploy a stack not contained' + ' in your local filesystem, please use the AWS console' + ' directly.'); } break; case 'cicd': if (!JsonValidation.validateConfig(rawConfig)) { console.warn(chalk_1.default.red('Error Initializing'), 'Invalid config file.'); return; } await this.deployCi(); break; case 'docs': this.runDocs(); break; case 'clean': await this.errorLogger.cleanMessages(); break; default: this.showHelp(); } } /** * Shows help for the CDK. */ showHelp() { return yargs_1.default // eslint-disable-line .scriptName('npx mira') .usage('Usage: npx mira COMMAND') .option('profile', { type: 'string', alias: 'p', desc: 'AWS profile name used for AWS CLI' }) .command('deploy', 'Deploys given stack', yargs => yargs.option('file', { type: 'string', alias: 'f', desc: 'REQUIRED: Path to your stack file', requiresArg: true })) .command('undeploy', 'Un-Deploys given stack', yargs => yargs.option('file', { type: 'string', alias: 'f', desc: 'REQUIRED: Path to your stack file', requiresArg: true })) .command('cicd', 'Deploys CI/CD pipeline', yargs => yargs .option('file', { type: 'string', aliast: 'f', desc: 'Path to permissions stack file.' }) .option('envVar', { type: 'string', desc: 'Environment variable passed into the code build' })) .command('docs', 'Starts local web server with documentation') .command('clean', 'Removes error log files') .command('domain', 'Deploys Domain Manager') .help() .demandCommand().argv; } /** * Undeploys a stack. This calls deploy with the undeploy parameter. The * only reason to do this is that both calls share almost identical code. */ async undeploy() { return await this.deploy(true); } /** * Changes context to use dev config if available, and runs passed function. * Typical usecase for this function is to set Dev environment in the context of Mira executable. * In case of CDK executions 'dev' is set as NODE_ENV during spawn. * @param fn * @param params */ useDevConfig(fn, params) { const tmpEnv = process.env.NODE_ENV || 'default'; process.env.NODE_ENV = 'dev'; const rsp = fn.apply(this, params); process.env.NODE_ENV = tmpEnv; return rsp; } getServiceStackName(account) { const tmpConfig = config_1.default.util.loadFileConfigs(path_1.default.join(process.cwd(), 'config')); return `${app_1.MiraApp.getBaseStackNameFromParams(tmpConfig.app.prefix, tmpConfig.app.name, 'Service')}-${account.name}`; } static getServiceStackName(account) { const tmpConfig = config_1.default.util.loadFileConfigs(path_1.default.join(process.cwd(), 'config')); return `${app_1.MiraApp.getBaseStackNameFromParams(tmpConfig.app.prefix, tmpConfig.app.name, 'Service')}-${account.name}`; } getAwsSdkConstruct(construct, account) { const credentials = new aws_sdk_1.default.SharedIniFileCredentials({ profile: this.getProfile(this.env) || '' }); aws_sdk_1.default.config.credentials = credentials; // eslint-disable-next-line // @ts-ignore return new aws_sdk_1.default[construct]({ region: account.env.region }); } async getFirstFailedNestedStackName(account, stackName) { var _a, _b; const cloudformation = this.getAwsSdkConstruct('CloudFormation', account); const events = await cloudformation .describeStackEvents({ StackName: stackName }) .promise(); return (_b = (_a = events.StackEvents) === null || _a === void 0 ? void 0 : _a.filter((event) => event.ResourceStatus === 'UPDATE_FAILED' || event.ResourceStatus === 'CREATE_FAILED')[0]) === null || _b === void 0 ? void 0 : _b.PhysicalResourceId; } async extractNestedStackError() { var _a; const account = mira_config_1.MiraConfig.getEnvironment(this.env); const stackName = this.useDevConfig(this.getServiceStackName, [account]); // Environment variable required to parse ~/.aws/config file with profiles. process.env.AWS_SDK_LOAD_CONFIG = '1'; let events; try { const nestedStackName = await this.getFirstFailedNestedStackName(account, stackName); const cloudformation = this.getAwsSdkConstruct('CloudFormation', account); events = await cloudformation .describeStackEvents({ StackName: nestedStackName }) .promise(); } catch (e) { console.log(chalk_1.default.red('Error, while getting error message from cloudformation. Seems something is wrong with your configuration.')); } const output = (_a = events === null || events === void 0 ? void 0 : events.StackEvents) === null || _a === void 0 ? void 0 : _a.filter((event) => event.ResourceStatus === 'UPDATE_FAILED' || event.ResourceStatus === 'CREATE_FAILED'); return output || []; } filterStackErrorMessages(errors) { const output = errors.filter(error => { return error.ResourceStatusReason !== 'Resource creation cancelled'; }); return output; } formatNestedStackError(item) { return `\n* ${item.ResourceStatus} - ${item.LogicalResourceId}\nReason: ${item.ResourceStatusReason}\nTime: ${item.Timestamp}\n`; } async printExtractedNestedStackErrors() { const printCarets = (nb) => { return '^'.repeat(nb); }; const failedResources = await this.extractNestedStackError(); if (Array.isArray(failedResources)) { console.log(chalk_1.default.red('\n\nYour app failed deploying, one of your nested stacks have failed to create or update resources. See the list of failed resources below:')); const filteredMessages = this.filterStackErrorMessages(failedResources); filteredMessages.map(errorMessage => { console.log(chalk_1.default.red(this.formatNestedStackError(errorMessage))); }); this.errorLogger.flushMessages(filteredMessages.map(m => this.formatNestedStackError(m))); console.log(chalk_1.default.red(`\n\n${printCarets(100)}\nAnalyze the list above, to find why your stack failed deployment.`)); } } async transpile() { if (this.stackFile.match(/.ts$/)) { const T = new transpiler_1.default(this.stackFile); const newFile = await T.run(); return newFile; } } } exports.MiraBootstrap = MiraBootstrap; //# sourceMappingURL=bootstrap.js.map