src/plugin-api.js
import * as swarmUtils from './swarm-utils';
import * as tasks from './tasks';
import * as utils from './utils';
import configValidator, { showDepreciations, showErrors } from './validate/index';
import { hooks, runRemoteHooks } from './hooks';
import { parseDockerInfo, StatusDisplay } from './status';
import chalk from 'chalk';
import childProcess from 'child_process';
import { cloneDeep } from 'lodash';
import { commands } from './commands';
import debug from 'debug';
import fs from 'fs';
import { getOptions } from './swarm-options';
import nodemiral from '@zodern/nodemiral';
import parseJson from 'parse-json';
import path from 'path';
import { runConfigPreps } from './prepare-config';
import { scrubConfig } from './scrub-config';
import serverInfo from './server-info';
const { resolvePath, moduleNotFoundIsPath } = utils;
const log = debug('mup:api');
export default class PluginAPI {
constructor(base, filteredArgs, program) {
this.base = program.config ? path.dirname(program.config) : base;
this.args = filteredArgs;
this._origionalConfig = null;
this.config = null;
this.settings = null;
this.sessions = null;
this._enabledSessions = program.servers ? program.servers.split(' ') : [];
this.configPath = program.config ? resolvePath(program.config) : path.join(this.base, 'mup.js');
this.settingsPath = program.settings;
this.verbose = program.verbose;
this.program = program;
this.commandHistory = [];
this.profileTasks = process.env.MUP_PROFILE_TASKS === 'true';
this.validationErrors = [];
this.resolvePath = utils.resolvePath;
this.getDockerLogs = utils.getDockerLogs;
this.runSSHCommand = utils.runSSHCommand;
this.forwardPort = utils.forwardPort;
this._createSSHOptions = utils.createSSHOptions;
this.statusHelpers = {
StatusDisplay,
parseDockerInfo
};
this.tasks = tasks;
}
getArgs() {
return this.args;
}
getBasePath() {
return this.base;
}
getVerbose() {
return this.verbose;
}
getOptions() {
return this.program;
}
hasMeteorPackage(name) {
// Check if app is using the package
try {
const contents = fs
.readFileSync(resolvePath(this.getBasePath(), this.getConfig().meteor.path, '.meteor/versions'))
.toString();
// Looks for "package-name@" in the beginning of a
// line or at the start of the file
const regex = new RegExp(`(^|\\s)${name}@`, 'm');
return regex.test(contents);
} catch (e) {
console.log(`Unable to load file ${resolvePath(this.getBasePath(), this.getConfig().meteor.path, '.meteor/versions')}`);
return false;
}
}
runTaskList(list, sessions, opts = {}) {
if (!('verbose' in opts)) {
opts.verbose = this.verbose;
}
if (!('showDuration' in opts)) {
opts.showDuration = this.profileTasks;
}
return utils.runTaskList(list, sessions, opts);
}
validateConfig(configPath, logProblems) {
// Only print errors once.
if (this.validationErrors.length > 0) {
return this.validationErrors;
}
const config = this.getConfig();
const {
errors,
depreciations
} = configValidator(config, this._origionalConfig);
const problems = [...errors, ...depreciations];
if (problems.length > 0 && logProblems) {
console.log(`loaded config from ${configPath}`);
console.log('');
if (errors.length) {
showErrors(errors);
}
if (depreciations.length) {
showDepreciations(depreciations);
}
console.log(
'Read the docs and view example configs at'
);
console.log(' http://meteor-up.com/docs');
console.log('');
}
this.validationErrors = problems;
return problems;
}
_normalizeConfig(config) {
if (typeof config !== 'object') {
return config;
}
if (config.meteor && typeof config.app !== 'object') {
config.app = Object.assign({}, config.meteor);
config.app.type = 'meteor';
} else if (typeof config.app === 'object' && !('type' in config.app)) {
config.app.type = 'meteor';
}
return runConfigPreps(config, this);
}
getConfig(validate = true) {
if (!this.config) {
try {
delete require.cache[require.resolve(this.configPath)];
// eslint-disable-next-line global-require
this.config = require(this.configPath);
this._origionalConfig = cloneDeep(this.config);
} catch (e) {
if (!validate) {
return {};
}
if (e.code === 'MODULE_NOT_FOUND' && moduleNotFoundIsPath(e, this.configPath)) {
console.error('"mup.js" file not found at');
console.error(` ${this.configPath}`);
console.error('Run "mup init" to create it.');
} else {
console.error(chalk.red('Error loading config file:'));
console.error(e);
}
process.exit(1);
}
this.config = this._normalizeConfig(this.config);
this.validateConfig(this.configPath, validate);
}
return this.config;
}
scrubConfig() {
const config = this.getConfig();
return scrubConfig(config);
}
getSettings() {
if (!this.settings) {
let filePath;
if (this.settingsPath) {
filePath = resolvePath(this.settingsPath);
} else {
filePath = path.join(this.base, 'settings.json');
}
this.settings = this.getSettingsFromPath(filePath);
}
return this.settings;
}
getSettingsFromPath(settingsPath) {
const filePath = resolvePath(settingsPath);
let settings;
try {
settings = fs.readFileSync(filePath).toString();
} catch (e) {
console.log(`Unable to load settings.json at ${filePath}`);
if (e.code !== 'ENOENT') {
console.log(e);
} else {
[
'It does not exist.',
'',
'You can create the file with "mup init" or add the option',
'"--settings path/to/settings.json" to load it from a',
'different location.'
].forEach(text => console.log(text));
}
process.exit(1);
}
try {
settings = parseJson(settings);
} catch (e) {
console.log('Error parsing settings file:');
console.log(e.message);
process.exit(1);
}
return settings;
}
setConfig(newConfig) {
this.config = newConfig;
}
_runHookScript(script) {
try {
childProcess.execSync(script, {
cwd: this.getBasePath(),
stdio: 'inherit'
});
} catch (e) {
console.log('Hook failed.');
process.exit(1);
}
}
_runHooks = async function(handlers, hookName) {
const messagePrefix = `> Running hook ${hookName}`;
for (const hookHandler of handlers) {
if (hookHandler.localCommand) {
console.log(`${messagePrefix} "${hookHandler.localCommand}"`);
this._runHookScript(hookHandler.localCommand);
}
if (typeof hookHandler.method === 'function') {
try {
await hookHandler.method(this, nodemiral);
} catch (e) {
this._commandErrorHandler(e);
}
}
if (hookHandler.remoteCommand) {
console.log(
`${messagePrefix} remote command "${hookHandler.remoteCommand}"`
);
await runRemoteHooks(
this.getConfig().servers,
hookHandler.remoteCommand
);
}
}
}
_runPreHooks = async function(name) {
const hookName = `pre.${name}`;
if (this.program['show-hook-names']) {
console.log(chalk.yellow(`Hook: ${hookName}`));
}
if (hookName in hooks) {
const hookList = hooks[hookName];
await this._runHooks(hookList, name);
}
};
_runPostHooks = async function(commandName) {
const hookName = `post.${commandName}`;
if (this.program['show-hook-names']) {
console.log(chalk.yellow(`Hook: ${hookName}`));
}
if (hookName in hooks) {
const hookList = hooks[hookName];
await this._runHooks(hookList, hookName);
}
};
_commandErrorHandler(e) {
log('_commandErrorHandler');
process.exitCode = 1;
// Only show error when not from nodemiral
// since nodemiral would have already shown the error
if (!(e.nodemiralHistory instanceof Array)) {
log('_commandErrorHandler: nodemiral error');
console.error(e.stack || e);
}
if (e.solution) {
console.log(chalk.yellow(e.solution));
}
process.exit(1);
}
runCommand = async function(name) {
const firstCommand = this.commandHistory.length === 0;
if (!name) {
throw new Error('Command name is required');
}
if (!(name in commands)) {
throw new Error(`Unknown command name: ${name}`);
}
this.commandHistory.push({ name });
await this._runPreHooks(name);
try {
log('Running command', name);
await commands[name].handler(this, nodemiral);
} catch (e) {
this._commandErrorHandler(e);
}
await this._runPostHooks(name).then(() => {
// The post hooks for the first command should be the last thing run
if (firstCommand) {
this._cleanupSessions();
}
});
}
async getServerInfo(selectedServers, collectors) {
if (this._cachedServerInfo && !collectors) {
return this._cachedServerInfo;
}
const serverConfig = this.getConfig().servers;
const servers = (
selectedServers || Object.keys(this.getConfig().servers)
).map(serverName => ({
...serverConfig[serverName],
name: serverName
}));
if (!collectors) {
console.log('');
console.log('=> Collecting Docker information');
}
const result = await serverInfo(servers, collectors);
if (!collectors) {
this._cachedServerInfo = result;
}
return result;
}
serverInfoStale() {
this._cachedServerInfo = null;
}
getSessions(modules = []) {
const sessions = this._pickSessions(modules);
return Object.keys(sessions).map(name => sessions[name]);
}
getSessionsForServers(servers = []) {
if (!this.sessions) {
this._loadSessions();
}
return servers.map(name => this.sessions[name]);
}
async getManagerSession() {
const { currentManagers } = await this.swarmInfo();
return this.getSessionsForServers(currentManagers)[0];
}
_pickSessions(plugins = []) {
if (!this.sessions) {
this._loadSessions();
}
const sessions = {};
plugins.forEach(moduleName => {
const moduleConfig = this.getConfig()[moduleName];
if (!moduleConfig) {
return;
}
for (const name in moduleConfig.servers) {
if (!moduleConfig.servers.hasOwnProperty(name)) {
continue;
}
if (this.sessions[name]) {
sessions[name] = this.sessions[name];
}
}
});
return sessions;
}
_loadSessions() {
const config = this.getConfig();
this.sessions = {};
// `mup.servers` contains login information for servers
// Use this information to create nodemiral sessions.
for (const name in config.servers) {
if (!config.servers.hasOwnProperty(name)) {
continue;
}
if (
this._enabledSessions.length > 0 &&
this._enabledSessions.indexOf(name) === -1
) {
continue;
}
const info = config.servers[name];
const auth = {
username: info.username
};
const opts = {
keepAlive: true,
ssh: info.opts || {}
};
const sshAgent = process.env.SSH_AUTH_SOCK;
opts.ssh.keepaliveInterval = opts.ssh.keepaliveInterval || 1000 * 28;
opts.ssh.keepaliveCountMax = opts.ssh.keepaliveCountMax || 12;
if (info.pem) {
try {
auth.pem = fs.readFileSync(resolvePath(info.pem), 'utf8');
} catch (e) {
console.error(`Unable to load pem at "${resolvePath(info.pem)}"`);
console.error(`for server "${name}"`);
if (e.code !== 'ENOENT') {
console.log(e);
}
process.exit(1);
}
} else if (info.password) {
auth.password = info.password;
} else if (sshAgent && fs.existsSync(sshAgent)) {
opts.ssh.agent = sshAgent;
} else {
console.error(
"error: server %s doesn't have password, ssh-agent or pem",
name
);
process.exit(1);
}
const session = nodemiral.session(info.host, auth, opts);
this.sessions[name] = session;
}
}
_cleanupSessions() {
log('cleaning up sessions');
if (!this.sessions) {
return;
}
Object.keys(this.sessions).forEach(key => {
this.sessions[key].close();
});
}
swarmEnabled() {
const config = this.getConfig();
return config.swarm && config.swarm.enabled;
}
async swarmInfo() {
const info = await this.getServerInfo();
const currentManagers = swarmUtils.currentManagers(info);
const desiredManagers = swarmUtils.desiredManagers(this.getConfig(), info);
const nodes = swarmUtils.findNodes(info);
const nodeIdsToServer = swarmUtils.nodeIdsToServer(info);
const desiredLabels = getOptions(this.getConfig()).labels;
const currentLabels = swarmUtils.currentLabels(info);
const clusters = swarmUtils.findClusters(info);
if (Object.keys(clusters).length > 1) {
swarmUtils.showClusters(clusters, nodeIdsToServer);
const error = new Error('multiple-clusters');
error.solution = 'The servers in your config are in multiple swarm clusters. Any servers already in a swarm cluster should be in the same cluster. Look above for the list of clusters.';
throw error;
}
return {
currentManagers,
desiredManagers,
nodes,
nodeIDs: nodeIdsToServer,
desiredLabels,
currentLabels
};
}
async dockerServiceInfo(serviceName) {
const manager = await this.getManagerSession();
if (!manager) {
const error = new Error('no-manager');
error.solution = 'Enable swarm in your config and run "mup setup"';
throw error;
}
const result = await this.runSSHCommand(manager, `sudo docker service inspect ${serviceName}`);
let serviceInfo = null;
try {
[serviceInfo] = JSON.parse(result.output);
} catch (e) {
// empty
}
return serviceInfo;
}
}