MitocGroup/run-jst

View on GitHub
components/terraform/src/terraform.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

const fse = require('fs-extra');
const dot = require('dot-object');
const path = require('path');
const execa = require('execa');
const Plan = require('./terraform/plan');
const State = require('./terraform/state');
const Downloader = require('./downloader');
const SecureOutput = require('./secure-output');
const Logger = require('recink/src/logger');
const { findFilesByPattern, versionCompare } = require('recink/src/helper/util');

/**
 * Terraform wrapper
 */
class Terraform {
  /**
   * @param {String} binary
   * @param {String} resource
   * @param {Object} vars
   * @param {Array} varFiles
   */
  constructor(
    binary = Terraform.BINARY,
    resource = Terraform.RESOURCE,
    vars = {},
    varFiles = [])
  {
    this.logger = Logger;
    this._binary = binary;
    this._resource = resource;
    this._vars = vars;
    this._varFiles = varFiles;
    this._isRemoteState = false;
    this._isWorkspaceSupported = false;
  }

  /**
   * @param {String} name
   * 
   * @returns {boolean} 
   */
  hasVar(name) {
    return this._vars.hasOwnProperty(name);
  }

  /**
   * @param {String} name
   * @param {*} defaultValue 
   * @returns {*}
   */
  getVar(name, defaultValue = null) {
    if (!this.hasVar(name)) {
      return defaultValue;
    }

    return this._vars[name];
  }

  /**
   * @param {String} name
   * @param {*} value 
   * 
   * @returns {Terraform}
   */
  setVar(name, value) {
    this._vars[name] = value;

    return this;
  }

  /**
   * @param {*} vars 
   * 
   * @returns {Terraform}
   */
  setVars(vars) {
    this._vars = vars;

    return this;
  }

  /**
   * @returns {String}
   */
  get getBinary() {
    return this._binary;
  }

  /**
   * @returns {String}
   */
  get getResource() {
    return this._resource;
  }

  /**
   * @returns {*}
   */
  get vars() {
    return this._vars;
  }

  /**
   * @returns {Array}
   */
  get varFiles() {
    return this._varFiles;
  }

  /**
   * @returns {Boolean}
   */
  get isWorkspaceSupported() {
    return this._isWorkspaceSupported;
  }

  /**
   * @returns {*}
   */
  get env() {
    const env = {};

    Object.keys(this.vars).forEach(name => {
      env[`TF_VAR_${ name }`] = this.vars[name];
    });

    return env;
  }

  /**
   * https://www.terraform.io/docs/commands/init.html
   * @param {String} dir
   * @returns {Promise}
   */
  init(dir) {
    return this
      .run('init', ['-no-color', '.'], dir)
      .then(() => this.checkRemoteState(dir))
      .then(() => this.pullState(dir))
      .then(() => Promise.resolve());
  }

  /**
   * https://www.terraform.io/docs/state/workspaces.html
   * @param {String} dir
   * @param {String} workspace
   * @returns {Promise}
   */
  workspace(dir, workspace) {
    return this._ensureResourceDir(dir).then(() => {
      let regex = RegExp(`(\\*\\s|\\s.)${workspace}$`, 'm');
      let options = ['new', workspace, '-no-color'];

      return this.run('workspace', ['list'], dir).then(result => {
        if (regex.exec(result.output) !== null) {
          options[0] = 'select';
        }

        if (fse.existsSync(`${dir}/terraform.tfstate.d`)) {
          this._resource = `terraform.tfstate.d/${workspace}`;
        }

        return this.run('workspace', options, dir);
      });
    });
  }

  /**
   * Check if remote state configured
   * @param {String} dir
   * @return {Promise}
   */
  checkRemoteState(dir) {
    const statePath = path.join(dir, '.terraform', Terraform.STATE);

    if (!fse.existsSync(statePath)) {
      return Promise.resolve();
    }

    return fse.readJson(statePath).then(stateObj => {
      this._isRemoteState = !!dot.pick('backend.type', stateObj);
      return Promise.resolve();
    });
  }

  /**
   * https://www.terraform.io/docs/commands/state/pull.html
   * @param {String} dir
   * @returns {Promise}
   */
  pullState(dir) {
    return this._ensureResourceDir(dir).then(() => {
      return this.run('state', ['pull'], dir).then(result => {
        if (this._isRemoteState && result.output) {
          const remoteStatePath = path.join(dir, this.getResource, Terraform.REMOTE);
          const backupStatePath = path.join(dir, this.getResource, Terraform.BACKUP);

          if (fse.existsSync(remoteStatePath)) {
            fse.moveSync(remoteStatePath, backupStatePath);
          }

          fse.writeFileSync(remoteStatePath, result.output, 'utf8');
        }

        return Promise.resolve();
      });
    });
  }

  /**
   * https://www.terraform.io/docs/commands/plan.html
   * @param {String} dir
   * @returns {Promise}
   */
  plan(dir) {
    return this._ensureResourceDir(dir).then(() => {
      const localStatePath = path.join(dir, this.getResource, Terraform.STATE);
      const planPath = path.join(dir, this.getResource, Terraform.PLAN);
      let options = ['-no-color', `-out=${planPath}`];

      this.varFiles.forEach(fileName => {
        options.push(`-var-file=${path.join(dir, fileName)}`);
      });

      if (!this._isRemoteState && fse.existsSync(localStatePath)) {
        options.push(`-state=${localStatePath}`);
      }

      return this.run('plan', options, dir).then(result => new Plan(planPath, result.output));
    });
  }

  /**
   * https://www.terraform.io/docs/commands/apply.html
   * @param {String} dir
   * @returns {Promise}
   */
  apply(dir) {
    return this._ensureResourceDir(dir).then(() => {
      const planPath = path.join(dir, this.getResource, Terraform.PLAN);
      const localStatePath = path.join(dir, this.getResource, Terraform.STATE);
      const remoteStatePath = path.join(dir, this.getResource, Terraform.REMOTE);
      const backupStatePath = path.join(dir, this.getResource, Terraform.BACKUP);
      let options = ['-no-color', '-auto-approve'];

      if (!this._isRemoteState && fse.existsSync(localStatePath)) {
        this.varFiles.forEach(fileName => {
          options.push(`-var-file=${path.join(dir, fileName)}`);
        });

        options.push(
          `-state=${ localStatePath }`,
          `-state-out=${ localStatePath }`,
          `-backup=${ backupStatePath }`
        );
      } else if (fse.existsSync(planPath)) {
        if (!this._isRemoteState) {
          options.push(`-state-out=${ localStatePath }`);
        }
        options.push(planPath);
      }

      return this.run('apply', options, dir).then(() => {
        if (this._isRemoteState) {
          return this.pullState(dir).then(() => Promise.resolve(new State(remoteStatePath, backupStatePath)));
        }

        return Promise.resolve(new State(localStatePath, backupStatePath));
      });
    });
  }

  /**
   * https://www.terraform.io/docs/commands/destroy.html
   * @param {String} dir
   * @returns {Promise}
   */
  destroy(dir) {
    return this._ensureResourceDir(dir).then(() => {
      const localStatePath = path.join(dir, this.getResource, Terraform.STATE);
      const backupStatePath = path.join(dir, this.getResource, Terraform.BACKUP);
      let options = ['-no-color', '-force'];

      this.varFiles.forEach(fileName => {
        options.push(`-var-file=${path.join(dir, fileName)}`);
      });

      if (!this._isRemoteState && fse.existsSync(localStatePath)) {
        options.push(
          `-state=${ localStatePath }`,
          `-state-out=${ localStatePath }`,
          `-backup=${ backupStatePath }`
        );
      }

      return this.run('destroy', options, dir).then(() => {
        let state = new State(localStatePath, backupStatePath);

        if (!this._isRemoteState) {
          return Promise.resolve(state);
        }

        return this.pullState(dir).then(() => Promise.resolve(state));
      });
    });
  }

  /**
   * https://www.terraform.io/docs/commands/show.html
   * 
   * @param {Plan|State} planOrState
   * @param {Boolean} secureOutput
   * @returns {Promise} 
   */
  show(planOrState, secureOutput = true) {
    let options = ['-no-color'];

    if (planOrState.path) {
      options.push(planOrState.path);
    }

    return this.run('show', options, planOrState.dir).then(result => {
      return Promise.resolve(
        secureOutput 
          ? SecureOutput.secure(result.output)
          : result.output
      );
    });
  }

  /**
   * @param {String} dir
   * @returns {Promise}
   * @private
   */
  _ensureResourceDir(dir) {
    return fse.ensureDir(path.join(dir, this.getResource));
  }

  /**
   * @param {String} command
   * @param {Array} args
   * @param {String} cwd
   * @returns {Promise}
   */
  run(command, args = [], cwd = process.cwd()) {
    const { env } = this;
    const bin = path.resolve(this.getBinary);
    const childProcess = execa(bin, [command].concat(args), { env, cwd });

    childProcess.stdout.on('data', data => {
      this.logger.info(SecureOutput.secure(data.toString() || ''));
    });

    this.logger.info(this.logger.emoji.magic, `Running ${this.getBinary} ${command} ${args.join(' ')} command`);
    this.logger.debug(this.logger.emoji.fire, findFilesByPattern(cwd, /^((?!(node_modules)).)*$/));

    return childProcess.then(({ stdout, code }) => Promise.resolve({ code, output: stdout }));
  }

  /**
   * Ensure binary exists (download otherwise)
   * @param {String} version
   * @returns {Promise}
   */
  ensure(version = Terraform.VERSION) {
    const compared = versionCompare(version, '0.11.0');
    this._isWorkspaceSupported = (compared >= 0);

    return fse.pathExists(this.getBinary).then(exists => {
      if (exists) {
        return Promise.resolve();
      }

      const downloader = new Downloader(version);
      const saveToDir = path.dirname(this.getBinary);

      return downloader.isVersionAvailable().then(isAvailable => {
        if (!isAvailable) {
          throw new Error(`Terraform version ${version} is not available`);
        }

        return downloader.download(saveToDir);
      });
    });
  }

  /**
   * @returns {String}
   */
  static get VERSION() {
    return '0.11.0';
  }

  /**
   * @returns {String}
   */
  static get PLAN() {
    return 'terraform.tfplan';
  }

  /**
   * @returns {String}
   */
  static get STATE() {
    return 'terraform.tfstate';
  }

  /**
   * @returns {String}
   */
  static get REMOTE() {
    return 'terraform.tfstate.remote';
  }

  /**
   * @returns {String}
   */
  static get BACKUP() {
    return `terraform.tfstate.${ new Date().getTime() }.backup`;
  }

  /**
   * @returns {String}
   */
  static get RESOURCE() {
    return '.resource';
  }

  /**
   * @returns {String}
   */
  static get BIN_PATH() {
    return path.resolve(process.cwd(), 'bin');
  }

  /**
   * @returns {String}
   */
  static get BIN_FILE() {
    return 'terraform';
  }

  /**
   * @returns {String}
   */
  static get BINARY() {
    return path.join(Terraform.BIN_PATH, Terraform.BIN_FILE);
  }
}

module.exports = Terraform;