TechnologyAdvice/DevLab

View on GitHub
src/command.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict'

const _ = require('halcyon')
const path = require('path')

const dewindowize = require('./dewindowize')

const command = {
  /**
   * @property {object} available args parsing instructions, matches config name
   * with command argument
   */
  args: {
    expose: '-p',
    volumes: '-v',
    env: '-e',
    hosts: '--add-host'
  },
  /**
   * Parses host environment variables specified with ${VAR}
   * @param {String} str The string to parse
   * @returns {String}
   */
  parseHostEnvVars: (str) => str.toString().replace(/\$\{([^}]+)\}/g, (i, match) => {
    const [envVar, defaultValue = ''] = match.split(':-')
    return process.env.hasOwnProperty(envVar) ? process.env[envVar] : defaultValue
  }),
  /**
   * Parses volumes to allow relative pathing from host mounts
   * @param {Array} vols The volume array to parse
   * @returns {Array}
   */
  parseVolumes: (vols) => vols.map((v) => v.startsWith('.') ? path.resolve(process.cwd(), v) : v),
  /**
   * Reduces args array into flagged arguments list
   * @param {string} type Name of the argument
   * @param {array} args Array of values
   * @returns {array}
   */
  parseArgs: (type, args) => {
    args = type === 'volumes' ? command.parseVolumes(args) : args
    return _.chain((item) => ([command.args[type], command.parseHostEnvVars(item)]), args)
  },
  /**
   * Parses config object and returns container name. Will have bc_ prefix and
   * InstanceID suffix if ephemeral, unaltered name for persisted containers
   * @param {object} cfg Config object
   * @returns {string}
   */
  getName: (name, cfg) => {
    if (cfg.persist) return name
    return `bc_${name}_${global.instanceId}`
  },
  /**
   * Parses config object and returns array of command arguments
   * @param {object} cfg Config object of instance or service
   * @returns {array} Command arguments
   */
  getArgs: (cfg) => _.pipe([
    _.keys,
    _.filter((key) => !!command.args[key]),
    _.chain(_.cond([
      [(key) => !_.isType('Array', cfg[key]), (key) => {
        throw new Error(`Config error: '${key}' should be an array`)
      }],
      [_.T, (key) => command.parseArgs(key, cfg[key])]
    ]))
  ])(cfg),
  /**
   * Returns array of execution commands
   * @param {object} cfg Config object for instance
   * @returns {string} Execution script
   */
  getExec: (cfg) => {
    const sh = '#!/bin/sh\nset -e;\n'
    const before = cfg.before ? `${cfg.before}\n` : ''
    const after = cfg.after ? `\n${cfg.after}` : ''
    // Custom exec, just run native task
    if (cfg.exec) return sh + before + cfg.exec + after
    // Ensure tasks exist
    if (!cfg.tasks) throw new Error('No tasks are defined')
    // Ensure a task is passed
    if (!cfg.run) throw new Error('No task has been specified')
    // Use predefined task(s)
    const run = _.pipe([
      tasks => _.pick(tasks, cfg.tasks),
      _.toPairs,
      _.map(([name, command]) => {
        if (!command) throw new Error(`Task '${name}' does not exist.`)
        if (_.isType('object', command)) {
          if (!command.cmd) throw new Error(`Task '${name}' has no command defined.`)
          return command.cmd
        }
        return command
      }),
      _.join('\n')
    ])(cfg.run)
    return sh + before + run + after
  },
  /**
   * Returns array of link arguments
   * @param {object} cfg Config object for the container
   * @returns {array} Link arguments
   */
  getLinks: (cfg) => _.chain(_.pipe([_.toPairs, _.head, ([key, value]) => {
    return ['--link', `${command.getName(key, value)}:${key}`]
  }]))(cfg.services || []),
  /**
   * Returns full command arguments array
   * @param {object} cfg Config object for instance
   * @param {string} name Container name
   * @param {string} tmpdir Path to temp execution file
   * @param {boolean} primary If this is primary, i.e. not a service container
   * @returns {object|array} Arguments for docker command
   */
  get: (cfg, name, tmpdir, primary = false) => {
    if (!cfg.from) throw new Error('Missing \'from\' property in config or argument')
    const cwd = dewindowize(process.cwd())
    const workDir = cfg.workDir || cwd
    let args
    let cmd
    if (primary) {
      // Running the main project container
      args = ['run', '--rm', '-v', `${cwd}:${workDir}:cached`, '-v', `${tmpdir}:${tmpdir}`, '-w', workDir]
      if (cfg.privileged !== false) args.push('--privileged')
      /* istanbul ignore else */
      if (process.stdout.isTTY) args.push('-it')
      cmd = ['sh', `${tmpdir}/binci.sh`]
    } else {
      // Running a service
      args = ['run', '-d']
      if (cfg.privileged !== false) args.push('--privileged')
      if (!cfg.rmOnShutdown) args.push('--rm')
      cmd = cfg.command
    }
    // Has user config
    if (cfg.user) args.push(`--user=${command.parseHostEnvVars(cfg.user)}`)
    // All other config
    args = args.concat(_.flatten([
      command.getArgs(cfg),
      command.getLinks(cfg),
      ['--name', command.getName(name, cfg)],
      cfg.from.toLowerCase(),
      cmd || []
    ]))
    return primary ? { args, cmd: command.getExec(cfg) } : args
  }
}

module.exports = command