binaris
Version:
Binaris SDK & CLI
369 lines (339 loc) • 11.5 kB
JavaScript
;
const { inspect } = require('util');
const deploy = require('./deploy');
const { create, experimentalRuntimes } = require('./create');
const feedback = require('./feedback');
const invoke = require('./invoke');
const list = require('./list');
const stats = require('./stats');
const logs = require('./logs');
const logger = require('./logger');
const perf = require('./perf');
const remove = require('./remove');
const YMLUtil = require('./binarisYML');
const {
getAllConf,
getAccountId,
updateAccountId,
updateAPIKey,
updateRealm,
validateHeaderValue,
} = require('./userConf');
const { auth, forceRealm } = require('../sdk');
const { validateName } = require('./nameUtil');
const sortBy = require('lodash.sortby');
const map = require('lodash.map');
// core modules
const fs = require('mz/fs');
const fse = require('fs-extra');
const path = require('path');
const inquirer = require('inquirer');
const PassThroughStream = require('stream').PassThrough;
const columnify = require('columnify');
/**
* Wrapper which centralizes the error handling/processing
* needed for the CLI. The two main purposes of this wrapper are...
*
* - formats and standardizes all errors which are thrown
* from the CLI/SDK
* - validates the incoming input(CLI input) and
* enforce rules(such as no sub-commands)
*
* @returns - error code of the command
*/
const exceptionWrapper = function tryCatchWrapper(funcToWrap) {
// eslint-disable-next-line consistent-return
return async function wrapped(options) {
try {
// git style sub commands are strictly not supported
// ie: bn create notacommand
// TODO(Ry): disabled until figure out how to do this in a cleaner way
// if (argData.args.length > 1) {
// throw new Error(`Argument "${argData.args[0]}"`,
// `is not a valid input to ${argData.rawArgs[2]}`);
// }
await funcToWrap(options);
process.exit(0);
} catch (err) {
logger.error(err.message);
process.exit(1);
}
};
};
/** Gather all of the relevant meta data about a function. */
const gatherMeta = async function gatherMeta(options, forcePath = true) {
const metaObj = {
path: path.resolve(options.path || process.cwd()),
};
metaObj.name = options.function;
metaObj.printPath = '';
if (forcePath) {
const binarisConf = await YMLUtil.loadBinarisConf(metaObj.path);
metaObj.name = metaObj.name || YMLUtil.getFuncName(await YMLUtil.loadBinarisConf(metaObj.path));
validateName(metaObj.name);
metaObj.conf = YMLUtil.getFuncConf(binarisConf, metaObj.name);
await YMLUtil.checkFuncConf(metaObj.conf, metaObj.path);
metaObj.printPath = options.path ? ` -p ${metaObj.path}` : '';
}
metaObj.printName = options.function ? ` ${metaObj.name}` : '';
return metaObj;
};
/**
* Create a Binaris function based on the options given by
* the user. This boils down to creating template files with the
* correct information in the correct location.
*
* @param {object} options - Command line options.
*/
const createHandler = async function createHandler(options) {
// this is where the actual create function is called and immediately
// evaluated to determine if was successfully completed
const config = { ...options.config };
if (options.executionModel) {
config.executionModel = options.executionModel;
}
const funcPath = path.resolve(options.path || process.cwd());
await fse.mkdirp(funcPath);
const funcName = await create(options.function, funcPath,
options.runtime, config);
const optPath = options.path ? ` -p ${funcPath}` : '';
const optName = options.function ? ` ${funcName}` : '';
logger.info(`Created function ${funcName} in ${funcPath}`);
if (config) {
logger.verbose(`with ${inspect(config)}`);
}
logger.info(` (use "bn deploy${optPath}${optName}" to deploy the function)`);
if (experimentalRuntimes.includes(options.runtime)) {
logger.warn(`${options.runtime} support is experimental`);
}
};
/**
* Deploys a previously created function to the Binaris cloud.
*
* @param {object} options - Command line options.
*/
const deployHandler = async function deployHandler(options) {
const meta = await gatherMeta(options);
const accountId = await getAccountId();
const endpoint = await deploy(meta.name, meta.path, meta.conf);
const apiHeader = '-H X-Binaris-Api-Key:$(bn show apiKey) ';
const printHeader = accountId !== undefined && !meta.name.startsWith('public_');
logger.info(
`Deployed function ${meta.name}
Invoke with one of:
"bn invoke${meta.printPath}${meta.printName}"
"curl ${printHeader ? apiHeader : ''}${endpoint}"`);
};
/**
* Removes a previously deployed function from the Binaris cloud.
*
* @param {object} options - Command line options.
*/
const removeHandler = async function removeHandler(options) {
await remove(options.function);
logger.info(`Removed function ${options.function}`);
};
/**
* Invokes a previously deployed Binaris function.
*
* @param {object} options - Command line options.
*/
const invokeHandler = async function invokeHandler(options) {
if (options.data && options.json) {
throw new Error('Invoke flags --json(-j) and --data(-d) are mutually exclusive');
}
let funcData;
if (options.data) {
funcData = options.data;
} else if (options.json) {
funcData = await fs.readFile(options.json, 'utf8');
}
const response = await invoke(options.function, funcData);
logger.info(response.body);
};
/**
* Run performance test on deployed function
*
* @param {object} options - Command line options.
*/
const perfHandler = async function perfHandler({
maxSeconds,
maxRequests,
concurrency,
data,
function: functionName,
}) {
const maybeS = n => (n > 1 ? 's' : '');
const upToString = maxSeconds ? ` up to ${maxSeconds} second${maybeS(maxSeconds)}` : '';
logger.info(`Running performance test on function ${functionName}.
Executing ${maxRequests} invocations with ${concurrency} "thread${maybeS(concurrency)}"${upToString}.
Stand by for results...
`);
const report = await perf(functionName, maxRequests, concurrency, data, maxSeconds);
logger.info('Perf summary');
logger.info('============');
logger.info(columnify({
'Total time': `${report.totalTimeSeconds.toFixed(1)} s`,
Invocations: report.totalRequests,
Errors: report.totalErrors,
Rate: `${report.rps.toFixed(1)} rps`,
}, { showHeaders: false }));
logger.info(`
Latencies
=========`);
logger.info(columnify({
Mean: `${report.meanLatencyMs.toFixed(1)} ms`,
Min: `${report.minLatencyMs.toFixed(1)} ms`,
Max: `${report.maxLatencyMs.toFixed(1)} ms`,
'50%': `${report.percentiles['50'].toFixed(1)} ms`,
'90%': `${report.percentiles['90'].toFixed(1)} ms`,
'95%': `${report.percentiles['95'].toFixed(1)} ms`,
'99%': `${report.percentiles['99'].toFixed(1)} ms`,
}, { showHeaders: false }));
};
/**
* List Binaris functions of given account
*
* @param {object} options - Command line options.
*/
const listHandler = async function listHandler(options) {
const listedFuncs = await list();
const rawData = map(listedFuncs, ({ tags }, key) => ({
name: key,
lastDeployed: tags.latest.modifiedAt,
expiration: tags.latest.expiration,
}));
if (options.json) {
logger.info(JSON.stringify(rawData));
} else if (rawData.length > 0) {
logger.info(columnify(rawData, {
columns: ['name', 'lastDeployed', 'expiration'],
config: {
name: {
headingTransform: () => 'FUNCTION',
minWidth: 25,
},
lastDeployed: {
headingTransform: () => 'LAST DEPLOYED',
},
},
}));
}
};
/**
* Retrieve logs from a deployed Binaris function.
*
* @param {object} options - Command line options.
*/
const logsHandler = async function logsHandler(options) {
const logStream = new PassThroughStream({ objectMode: true });
let lineAcc = [];
logStream.on('data', (currLog) => {
if (currLog.msg.endsWith('\n')) {
logger.info(`[${currLog.time}] ${lineAcc.join('')}${currLog.msg.slice(0, -1)}`);
lineAcc = [];
} else {
lineAcc.push(currLog.msg);
}
});
await logs(options.function, options.tail, options.since, logStream);
};
/**
* Retrieve account usage stats
*
* @param {object} options - Command line options.
*/
const statsHandler = async function statsHandler(options) {
const metrics = await stats(options.since, options.until);
// eslint-disable-next-line no-param-reassign
metrics.metrics.forEach((m) => { delete m.account; });
const columns = ['function', 'metric', 'value', 'since', 'until'];
metrics.metrics = sortBy(metrics.metrics, columns);
if (options.json) {
logger.info(JSON.stringify(metrics));
} else if (metrics.metrics.length === 0) {
logger.info('No matching usage stats found');
} else {
logger.info(columnify(metrics.metrics, {
showHeaders: true,
columns,
}));
}
};
/**
* Show information about configured account.
*/
const showHandler = async function showHandler({ config, all }) {
const conf = await getAllConf();
if (all) {
Object.keys(conf).forEach((confKey) => {
logger.info(`${confKey}: ${conf[confKey]}`);
});
} else {
logger.info(conf[config]);
}
};
/**
* Authenticate the user by saving the provided Binaris
* api key in a well known .binaris directory.
*/
const loginHandler = async function loginHandler() {
logger.info(
`Please enter your Binaris API key to deploy and invoke functions.
If you don't have a key, head over to https://binaris.com to request one`);
const { rawApiKey } = await inquirer.prompt([
{
type: 'password',
name: 'rawApiKey',
message: 'API Key:',
},
]);
validateHeaderValue(rawApiKey, 'API key');
const apiKeyIdx = rawApiKey.lastIndexOf('-');
const apiKey = rawApiKey.substr(apiKeyIdx + 1);
const realm = rawApiKey.substr(0, apiKeyIdx);
if (realm) {
forceRealm(realm);
}
const { accountId, error } = await auth.verifyAPIKey(apiKey);
// (Don't validate accountId, it comes from Binaris so not a
// keyboard entry issue.)
if (error) {
throw error;
}
await updateAccountId(accountId);
await updateAPIKey(apiKey);
await updateRealm(realm);
logger.info(
`Authentication Succeeded
(use "bn create node8 hello" to create a Node.js template function in your CWD)`);
};
/**
* Allow feedback by user of a given account.
*
* @param {object} options - Command line options.
*/
const feedbackHandler = async function feedbackHandler(options) {
if (!options.email || options.email.length === 0) {
throw new Error('Not a valid email.');
}
if (!options.message || options.message.length === 0) {
throw new Error('Not a valid message.');
}
const funcData = { email: options.email, message: options.message };
await feedback(funcData);
logger.info('Thank you!');
};
module.exports = {
deployHandler: exceptionWrapper(deployHandler),
createHandler: exceptionWrapper(createHandler),
feedbackHandler: exceptionWrapper(feedbackHandler),
invokeHandler: exceptionWrapper(invokeHandler),
listHandler: exceptionWrapper(listHandler),
logsHandler: exceptionWrapper(logsHandler),
loginHandler: exceptionWrapper(loginHandler),
perfHandler: exceptionWrapper(perfHandler),
removeHandler: exceptionWrapper(removeHandler),
statsHandler: exceptionWrapper(statsHandler),
showHandler: exceptionWrapper(showHandler),
};