UNPKG

hab-client

Version:

Promise-based habitat client that mostly just executes the hab binary but can also query the supervisor API

361 lines (300 loc) 11.3 kB
const semver = require('semver'); const _ = require('underscore'); const axios = require('axios'); const child_process = require('child_process'); const logger = require('./logger'); /** * Represents and provides an interface to an executable hab binary * available in the host environment */ class Hab { constructor ({ command = null, supervisorApi = null } = {}) { this.command = command || this.command; this.supervisorApi = axios.create({ baseURL: supervisorApi || this.supervisorApi }); this.version = null; this.build = null; } /** * Get the version of the hab binary * @returns {?string} Version reported by habitat binary, or null if not available */ async getVersion () { if (this.version === null) { try { const output = await this.exec({ version: true }); [, this.version, this.build] = /^hab ([^\/]+)\/(\d+)$/.exec(output); } catch (err) { this.version = false; } } return this.version || null; } /** * Check if hab version is satisfied * @param {string} range - The version or range habitat should satisfy (see https://github.com/npm/node-semver#ranges) * @returns {boolean} True if habitat version satisfies provided range */ async satisfiesVersion (range) { return semver.satisfies(await this.getVersion(), range); } /** * Ensure that hab version is satisfied * @param {string} range - The version or range habitat should satisfy (see https://github.com/npm/node-semver#ranges) * @returns {Git} Returns current instance or throws exception if version range isn't satisfied */ async requireVersion (range) { if (!await this.satisfiesVersion(range)) { throw new Error(`Habitat version must be ${range}, reported version is ${await this.getVersion()}`); } return this; } /** * @returns {Object[]|false} Array of service status objects or false if the supervisor is unavailable */ async getSupervisorStatus () { try { const output = (await this.exec('svc', 'status')).split(/\n/); if (output.length == 1) { return []; } let columns; const services = []; for (let line of output) { line = line.split(/\s{2,}/); if (!columns) { columns = line; continue; } services.push(_.object(columns, line)); } return services; } catch (err) { return false; } } /** * Gets axios instance for supervisor API * @returns {Object} axios */ getSupervisorApi () { return this.supervisorApi; } /** * Gets array of services via supervisor API * @returns {Object[]} Array of services */ async getServices () { const response = await this.getSupervisorApi().get('services'); return response.data; } /** * Executes habitat with given arguments * @param {string|string[]} args - Arguments to execute * @param {?Object} execOptions - Extra execution options * @returns {Promise} */ async exec (...args) { let command; const commandArgs = []; const commandEnv = {}; const execOptions = { maxBuffer: 1024 * 1024 // 1 MB output buffer }; // scan through all arguments let arg; while (arg = args.shift()) { switch (typeof arg) { case 'string': if (!command) { command = arg; // the first string is the command break; } // fall through and get pushed with numbers case 'number': commandArgs.push(arg.toString()); break; case 'object': // clone before deleting processed keys arg = Object.assign({}, arg); // extract any general execution options if ('$nullOnError' in arg) { execOptions.nullOnError = arg.$nullOnError; delete arg.$nullOnError; } if ('$spawn' in arg) { execOptions.spawn = arg.$spawn; delete arg.$spawn; } if ('$shell' in arg) { execOptions.shell = arg.$shell; delete arg.$shell; } if ('$cwd' in arg) { execOptions.cwd = arg.$cwd; delete arg.$cwd; } if ('$env' in arg) { for (let key in arg.$env) { commandEnv[key] = arg.$env[key]; } delete arg.$env; } if ('$preserveEnv' in arg) { execOptions.preserveEnv = arg.$preserveEnv; delete arg.$preserveEnv; } if ('$options' in arg) { for (let key in arg.$options) { execOptions[key] = arg.$options[key]; } delete arg.$options; } if ('$passthrough' in arg) { if (execOptions.passthrough = Boolean(arg.$passthrough)) { execOptions.spawn = true; } delete arg.$passthrough; } if ('$wait' in arg) { execOptions.wait = Boolean(arg.$wait); delete arg.$wait; } // any remaiing elements are args/options for (let key in arg) { const value = arg[key]; if (key.length == 1) { if (value === true) { commandArgs.push('-'+key); } else if (value !== false) { commandArgs.push('-'+key, value); } } else { if (value === true) { commandArgs.push('--'+key); } else if (value !== false) { commandArgs.push('--'+key, value); } } } break; default: throw 'unhandled exec argument'; } } // prefixs args with command if (command) { commandArgs.unshift(command); } // prepare environment if (execOptions.preserveEnv !== false) { Object.setPrototypeOf(commandEnv, process.env); } execOptions.env = commandEnv; // execute git command logger.debug(this.command, commandArgs.join(' ')); if (execOptions.spawn) { const process = child_process.spawn(this.command, commandArgs, execOptions); if (execOptions.passthrough) { process.stdout.on('data', data => data.toString().trim().split(/\n/).forEach(line => logger.info(line))); process.stderr.on('data', data => data.toString().trim().split(/\n/).forEach(line => logger.error(line))); } if (execOptions.wait) { return new Promise((resolve, reject) => { process.on('exit', code => { if (code == 0 || execOptions.nullOnError) { resolve(); } else { reject(code); } }); }); } let capturePromise; process.captureOutput = (input = null) => { if (!capturePromise) { capturePromise = new Promise((resolve, reject) => { let output = ''; process.stdout.on('data', data => { output += data; }); process.on('exit', code => { if (code == 0 || execOptions.nullOnError) { resolve(output); } else { reject({ output, code }); } }); }); } if (input) { process.stdin.write(input); process.stdin.end(); } return capturePromise; }; process.captureOutputTrimmed = async (input = null) => { return (await process.captureOutput(input)).trim(); }; return process; } else if (execOptions.shell) { return new Promise((resolve, reject) => { child_process.exec(`${this.command} ${commandArgs.join(' ')}`, execOptions, (error, stdout, stderr) => { if (error) { if (execOptions.nullOnError) { return resolve(null); } else { error.stderr = stderr; return reject(error); } } resolve(stdout.trim()); }); }); } else { return new Promise((resolve, reject) => { child_process.execFile(this.command, commandArgs, execOptions, (error, stdout, stderr) => { if (error) { if (execOptions.nullOnError) { return resolve(null); } else { error.stderr = stderr; return reject(error); } } resolve(stdout.trim()); }); }); } } } // set default habitat command Hab.prototype.command = 'hab'; Hab.prototype.supervisorApi = 'http://localhost:9631'; // add first-class methods for common hab subcommands [ 'bldr', 'cli', 'config', 'file', 'help', 'origin', 'pkg', 'plan', 'ring', 'studio', 'sup', 'support-bundle', 'svc', 'user' ].forEach(command => { const method = command.replace(/-([a-zA-Z])/, (match, letter) => letter.toUpperCase()); command = method.toLowerCase(); Hab.prototype[method] = function (...args) { args.unshift(command); return this.exec.apply(this, args); }; }); // export class module.exports = Hab;