terraform-plus
Version:
Terraform Plus
384 lines (332 loc) • 10.6 kB
JavaScript
'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;