UNPKG

terraform-plus

Version:
384 lines (332 loc) 10.6 kB
'use strict'; const fs = require('fs-extra'); const path = require('path'); const twig = require('twig'); const yaml = require('js-yaml'); const fuzzy = require('fuzzy'); const AbstractCommand = require('../abstract-command'); class CreateCommand extends AbstractCommand { /** * @desc Validates options passed via cli * * @returns {Boolean} - Returns true if options given from cli are valid. * * @private */ _validateOptions() { if (this._cli.interactive) { return true; } if (this._cli.rawArgs.indexOf('-h') > -1 || this._cli.rawArgs.indexOf('--help') > -1) { this._cli.outputHelp(); process.exit(0); } if (!this._cli.template) { this._logger.warn(`--template is required. ${this._suffix}`); return false; } else if (!this.templatePath) { this._logger.error(`--template or --provider is invalid: ${this._cli.template}. ${this._suffix}`); return false; } if (!this._cli.id) { this._logger.warn(`--id is required. ${this._suffix}`); return false; } return super._validateOptions(); } /** * @desc * Creates terraform templates step by step for each file in specified directory * @param {Object} replacements - an object of structure name: _cli.id */ run() { this.failedToRun = false; this._logger.debug(`Provider: ${this._cli.provider}`); this._logger.debug(`Template: ${this._cli.template}`); this._logger.debug(`Templates of provider: ${this._templates}`); const templatePath = `${CreateCommand.TEMPLATES_PATH}/${this.templatePath}`; const replacements = { name: this._cli.id }; let folder = `${this._directory}/${this._cli.id}`; if (this._cli.force && fs.existsSync(folder)) { fs.removeSync(folder); } fs.mkdir(folder, (err) => { if (err) { this._logger.error(`No such directory or file already exists: ${this._directory}/\n${err}`); this.failedToRun = true; return new Error(err); } }); fs.writeFile(`${this._directory}/${this._cli.id}/${CreateCommand.TFS_CONFIG}`, this.config, function(err) { if (err) { this._logger.error(`No such directory or file already exists: ${this._directory}/\n${err}`); return new Error(err); } }); let files = this.readDirR(templatePath); files.forEach(file => { let fileName = path.basename(file); let filePath = file.replace(templatePath + '/', ''); if (path.extname(file) === '.twig') { this.makeTemplate(`${file}`, replacements, fileName, `${this._directory}/${this._cli.id}`); } else if (fileName === CreateCommand.TFS_TFVAR) { this.copyFile(`${file}`, `${this._directory}/${this._cli.id}/${fileName}`) .then(() => { this.addVars(fileName); }); } else if (fileName === CreateCommand.TFS_DEPLOY_CONFIG) { let paramFolder = `${this._directory}/${this._cli.id}/${CreateCommand.TFS_PATH}`; if (!fs.existsSync(paramFolder)) { fs.mkdir(paramFolder, (err) => { if (err) { this._logger.error(`No such directory or file already exists: ${this._directory}/\n${err}`); this.failedToRun = true; return new Error(err); } }); } this.copyFile(`${file}`, `${paramFolder}/${fileName}`); } else { fs.ensureDirSync(path.dirname(`${this._directory}/${this._cli.id}/${filePath}`)); this.copyFile(`${file}`, `${this._directory}/${this._cli.id}/${filePath}`); } }); this._logger.info(`${this._cli.path}/${this._cli.id} was created successfully`); return Promise.resolve(); } /** * * @param {String} dir * * @return {T[]} */ readDirR(dir) { return fs.statSync(dir).isDirectory() ? Array.prototype.concat(...fs.readdirSync(dir).map(f => this.readDirR(path.join(dir, f)))) : dir; } /** * * @param {String} varFile */ addVars(varFile) { if (this._cli.var) { let varFilePath = `${this._directory}/${this._cli.id}/${varFile}`; let variables = fs.createWriteStream(varFilePath, { flags: 'a' // 'a' means appending (old data will be preserved) }); variables.write('\n'); this._cli.var.forEach(variable => { variables.write(variable + '\n'); }); variables.end(); } } /** * * @param {String} source * @param {String} target * @return {Promise<any>} */ copyFile(source, target) { var rd = fs.createReadStream(source); var wr = fs.createWriteStream(target); return new Promise(function(resolve, reject) { rd.on('error', reject); wr.on('error', reject); wr.on('finish', resolve); rd.pipe(wr); }).catch(function(error) { rd.destroy(); wr.end(); throw error; }); } /** * @desc Makes template files. Parses .twig files and creates new .tf files * * @param {String} pathToFile - a specific path to file's template * @param {Object} replacements - specific item that should be replaced * @param {String} fileName - file.twig that is currently parsed. * @param {String} newPath - Path where the template file will be created. */ makeTemplate(pathToFile, replacements, fileName, newPath) { let logger = this._logger; let fs = this._fs; twig.renderFile(pathToFile, replacements, (err, template) => { if (err) { logger.error(`Invalid path indicated: ${pathToFile}\n${err}`); return new Error(err); } fileName = fileName.replace('.twig', ''); if (fileName === CreateCommand.TFS_TFVAR) { this._cli.var.forEach(variable => { template = template + variable + '\n'; }); } let twigFile = fs.createWriteStream(`${newPath}/${fileName}`); twigFile.write(template); }); } /** * @desc Find template key. Parses object and return path of template * * @param {Object} templates - templates object * @param {Object} providers - providers object * @param {String} key - name of specific template * @param {String} provider - provider of specific template * * @returns {(String|Boolean)} retruns path of template or false if template not exist */ get templatePath() { let templates = this._templates; let providers = this._providers; let key = this._cli.template; let provider = this._cli.provider; if (key.includes('@')) { let [template, provider] = key.split('@'); if (providers.hasOwnProperty(provider)) { let providerName = providers[provider]; this._provider = providerName; return templates[providerName][template]; } } else if (provider) { if (providers.hasOwnProperty(provider)) { let providerName = providers[provider]; return templates[providerName][key]; } } else { for (let provider in templates) { if (templates[provider].hasOwnProperty(key)) { this._provider = provider; return templates[provider][key]; } } } return false; } get config() { let config = {}; config.provider = this._provider; config.id = this._cli.id; if (this._cli.parent) { config.parent = this._cli.parent; } return yaml.safeDump(config); } promt(answers) { this._cli.template = `${answers.template}@${answers.provider}`; this._cli.id = answers.id; if (answers.parent) { this._cli.parent = answers.parent; } if (answers.directory) { this._cli.directory = answers.directory; } if (answers.var) { this._cli.var = answers.var.split(','); } } /** * * @param {Object} answers * @param {String} input * @return {Promise<any>} */ static searchTemplate(answers, input) { let mapping = AbstractCommand.TEMPLATES_MAPPING; let provider = Object.keys(mapping.template[answers.provider]); input = input || ''; return new Promise(function(resolve) { var fuzzyResult = fuzzy.filter(input, provider); resolve(fuzzyResult.map(function(el) { return el.original; })); }); } /** * @returns {Array} */ static get QUESTIONS() { let mapping = AbstractCommand.TEMPLATES_MAPPING; return [{ type: 'rawlist', name: 'provider', message: 'Select provider: ', choices: Object.keys(mapping.template) }, { type: 'autocomplete', name: 'template', message: 'Select resource: ', source: CreateCommand.searchTemplate }, { type: 'input', name: 'id', message: 'Input resource name: ', validate: function(value) { if (value) { return true; } return 'Please enter a valid resource name'; } }, { type: 'input', name: 'parent', message: 'Input parent name (optional): ' }, { type: 'input', name: 'directory', message: 'Input directory (optional): ', validate: function(value) { if (value.length > 0) { if (!fs.existsSync(value)) { return 'Access denied to directory or directory not exists'; } } return true; } }, { type: 'input', name: 'var', message: 'Input custom variables comma separated (optional): ' }]; } /** * @returns {String} */ static get DESCRIPTION() { return 'create terraform script from predefined template'; } /** * @returns {Array} */ static get OPTIONS() { return [{ opt: '-i, --id <name>', desc: 'alphanumeric value to be used as uniquely identifiable cloud resource name' }, { opt: '-P, --parent [name]', desc: 'name of cloud resource that will be provisioned before current cloud resource' }, { opt: '-t, --template <template>', desc: 'specify the template name (e.g. api-gateway, cloudfront, dynamodb, lambda, s3)' }, { opt: '-p, --provider [provider]', desc: 'optional value to match terraform provider (e.g. aws, azurerm, google)' }, { opt: '-d, --directory [directory]', desc: 'path where template should be created (default value - current working directory)' }, { opt: '-f, --force', desc: 'replace directory' }, { opt: '-I, --interactive', desc: 'run in interactive mode' }, { opt: '-r, --var [list]', desc: 'custom variables', collect: 'true' }]; } } module.exports = CreateCommand;