From 112f191e681ffbfd0ffae56ca28e8e600c52f255 Mon Sep 17 00:00:00 2001 From: peterdemartini Date: Thu, 8 Nov 2018 08:11:41 -0700 Subject: [PATCH] API changes --- .../teraslice/lib/cluster/services/api.js | 567 ++++++------------ .../teraslice/lib/cluster/services/assets.js | 78 ++- .../lib/processors/stdout/processor.js | 7 +- packages/teraslice/lib/utils/api_utils.js | 49 +- packages/teraslice/package.json | 1 + packages/teraslice/test/services/api-spec.js | 81 +++ yarn.lock | 89 ++- 7 files changed, 439 insertions(+), 433 deletions(-) create mode 100644 packages/teraslice/test/services/api-spec.js diff --git a/packages/teraslice/lib/cluster/services/api.js b/packages/teraslice/lib/cluster/services/api.js index a570b45c370..0b7e04015bc 100644 --- a/packages/teraslice/lib/cluster/services/api.js +++ b/packages/teraslice/lib/cluster/services/api.js @@ -5,23 +5,24 @@ const { Router } = require('express'); const Promise = require('bluebird'); const bodyParser = require('body-parser'); const request = require('request'); -const util = require('util'); const { makePrometheus, isPrometheusRequest, makeTable, sendError, - handleError, - getSearchOptions + handleRequest, + getSearchOptions, } = require('../../utils/api_utils'); +const makeStateStore = require('../storage/state'); const terasliceVersion = require('../../../package.json').version; -module.exports = function module(context, app, { assetsUrl }) { +module.exports = async function makeAPI(context, app, options) { + const { assetsUrl, stateStore: _stateStore } = options; const logger = context.apis.foundation.makeLogger({ module: 'api_service' }); const executionService = context.services.execution; const jobsService = context.services.jobs; const v1routes = new Router(); - let stateStore; + const stateStore = _stateStore || await makeStateStore(context); app.use(bodyParser.json({ type(req) { @@ -37,22 +38,28 @@ module.exports = function module(context, app, { assetsUrl }) { } }); + app.use((req, res, next) => { + req.logger = logger; + next(); + }); + app.set('json spaces', 4); v1routes.get('/', (req, res) => { - const responseObj = { + const requestHandler = handleRequest(req, res); + requestHandler(() => ({ arch: context.arch, clustering_type: context.sysconfig.teraslice.cluster_manager_type, name: context.sysconfig.teraslice.name, node_version: process.version, platform: context.platform, teraslice_version: `v${terasliceVersion}` - }; - res.status(200).json(responseObj); + })); }); v1routes.get('/cluster/state', (req, res) => { - res.status(200).json(executionService.getClusterState()); + const requestHandler = handleRequest(req, res); + requestHandler(() => executionService.getClusterState()); }); v1routes.route('/assets*') @@ -79,38 +86,25 @@ module.exports = function module(context, app, { assetsUrl }) { const jobSpec = req.body; const shouldRun = start !== 'false'; - logger.trace(`POST /jobs endpoint has received shouldRun: ${shouldRun}, job:`, jobSpec); - const handleApiError = handleError(res, logger, 500, 'Job submission failed'); + const requestHandler = handleRequest(req, res, 'Job submission failed'); - jobsService.submitJob(jobSpec, shouldRun) - .then((ids) => { - res.status(202).json(ids); - }) - .catch(handleApiError); + requestHandler(() => jobsService.submitJob(jobSpec, shouldRun)); }); v1routes.get('/jobs', (req, res) => { const { size, from, sort } = getSearchOptions(req); - logger.trace(`GET /jobs endpoint has been called, from: ${from}, size: ${size}, sort: ${sort}`); - const handleApiError = handleError(res, logger, 500, 'Could not retrieve list of jobs'); - - jobsService.getJobs(from, size, sort) - .then((results) => { - res.status(200).json(results); - }) - .catch(handleApiError); + const requestHandler = handleRequest(req, res, 'Could not retrieve list of jobs'); + requestHandler(() => jobsService.getJobs(from, size, sort)); }); v1routes.get('/jobs/:jobId', (req, res) => { const { jobId } = req.params; - logger.trace(`GET /jobs/:jobId endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, 'Could not retrieve job'); + const requestHandler = handleRequest(req, res, 'Could not retrieve job'); - jobsService.getJob(jobId) - .then(jobSpec => res.status(200).json(jobSpec)) - .catch(handleApiError); + + requestHandler(async () => jobsService.getJob(jobId)); }); v1routes.put('/jobs/:jobId', (req, res) => { @@ -122,345 +116,166 @@ module.exports = function module(context, app, { assetsUrl }) { return; } - logger.trace(`PUT /jobs/:jobId endpoint has been called, job_id: ${jobId}, update changes: `, jobSpec); - const handleApiError = handleError(res, logger, 500, 'Could not update job'); + const requestHandler = handleRequest(req, res, 'Could not update job'); - jobsService.updateJob(jobId, jobSpec) - .then(status => res.status(200).json(status)) - .catch(handleApiError); + requestHandler(async () => jobsService.updateJob(jobId, jobSpec)); }); v1routes.get('/jobs/:jobId/ex', (req, res) => { const { jobId } = req.params; - logger.trace(`GET /jobs/:jobId endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, 'Could not retrieve list of execution contexts'); + const requestHandler = handleRequest(req, res, 'Could not retrieve list of execution contexts'); - jobsService.getLatestExecution(jobId) - .then(execution => res.status(200).json(execution)) - .catch(handleApiError); + requestHandler(async () => jobsService.getLatestExecution(jobId)); }); v1routes.post('/jobs/:jobId/_start', (req, res) => { const { jobId } = req.params; + const requestHandler = handleRequest(req, res, `Could not start job: ${jobId}`); - logger.trace(`GET /jobs/:jobId/_start endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, `Could not start job: ${jobId}`); - - jobsService.startJob(jobId) - .then(ids => res.status(200).json(ids)) - .catch(handleApiError); + requestHandler(async () => jobsService.startJob(jobId)); }); - v1routes.post('/jobs/:jobId/_stop', (req, res) => { - const { jobId } = req.params; + v1routes.post(['/jobs/:jobId/_stop', '/ex/:exId/_stop'], (req, res) => { const { timeout, blocking = true } = req.query; - logger.trace(`POST /jobs/:jobId/_stop endpoint has been called, job_id: ${jobId}, removing any pending workers for the job`); - const handleApiError = handleError(res, logger, 500, `Could not stop execution for job: ${jobId}`); + const requestHandler = handleRequest(req, res, 'Could not stop execution'); - jobsService.getLatestExecutionId(jobId) - .then(exId => executionService.stopExecution(exId, timeout) - .then(() => _waitForStop(exId, blocking)) - .then(status => res.status(200).json({ status }))) - .catch(handleApiError); + requestHandler(async () => { + const exId = await _getExIdFromRequest(req); + await executionService.stopExecution(exId, timeout); + return _waitForStop(exId, blocking); + }); }); - v1routes.post('/jobs/:jobId/_pause', (req, res) => { - const { jobId } = req.params; - - logger.trace(`POST /jobs/:jobId/_pause endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, `Could not pause execution for job: ${jobId}`); + v1routes.post(['/jobs/:jobId/_pause', '/ex/:exId/_pause'], (req, res) => { + const requestHandler = handleRequest(req, res, 'Could not pause execution'); - jobsService.pauseJob(jobId) - .then(body => res.status(200).json(body)) - .catch(handleApiError); + requestHandler(async () => { + const exId = await _getExIdFromRequest(req); + await executionService.getActiveExecution(exId); + return executionService.pauseExecution(exId); + }); }); - v1routes.post('/jobs/:jobId/_resume', (req, res) => { - const { jobId } = req.params; - - logger.trace(`POST /jobs/:jobId/_resume endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, `Could not resume execution for job: ${jobId}`); + v1routes.post(['/jobs/:jobId/_resume', '/ex/:exId/_resume'], (req, res) => { + const requestHandler = handleRequest(req, res, 'Could not resume execution'); - jobsService.resumeJob(jobId) - .then(body => res.status(200).json(body)) - .catch(handleApiError); + requestHandler(async () => { + const exId = await _getExIdFromRequest(req); + await executionService.getActiveExecution(exId); + return executionService.resumeExecution(exId); + }); }); - v1routes.post('/jobs/:jobId/_recover', (req, res) => { - const { jobId } = req.params; + v1routes.post(['/jobs/:jobId/_recover', '/ex/:exId/_recover'], (req, res) => { const { cleanup } = req.query; - logger.trace(`POST /jobs/:jobId/_recover endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, `Could not recover execution for job: ${jobId}`); + const requestHandler = handleRequest(req, res, 'Could not recover execution'); if (cleanup && !(cleanup === 'all' || cleanup === 'errors')) { - res.status(400).json({ error: 'if cleanup is specified it must be set to "all" or "errors"' }); + const errorMsg = 'if cleanup is specified it must be set to "all" or "errors"'; + res.status(400).json({ error: errorMsg }); return; } - jobsService.recoverJob(jobId, cleanup) - .then(body => res.status(200).json(body)) - .catch(handleApiError); + requestHandler(async () => { + const exId = await _getExIdFromRequest(req); + return executionService.recoverExecution(exId, cleanup); + }); }); - v1routes.post('/jobs/:jobId/_workers', (req, res) => { + v1routes.post(['/jobs/:jobId/_workers', '/ex/:exId/_workers'], (req, res) => { const { query } = req; - const { jobId } = req.params; - logger.trace('POST /jobs/:jobId/_workers endpoint has been called, query:', query); - const handleApiError = handleError(res, logger, 500, `Could not change workers for job: ${jobId}`); - - _changeWorkers('job', jobId, query) - .then(responseObj => res.status(200).send(`${responseObj.workerNum} workers have been ${responseObj.action} for execution: ${responseObj.ex_id}`)) - .catch(handleApiError); - }); + const requestHandler = handleRequest(req, res, 'Could not change workers count'); - v1routes.get('/jobs/:jobId/slicer', _deprecateSlicerName((req, res) => { - const { jobId } = req.params; - - logger.trace(`GET /jobs/:jobId/slicer endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, `Could not get slicer statistics for job: ${jobId}`); - - jobsService.getLatestExecutionId(jobId) - .then(exId => _controllerStats(exId)) - .then(results => res.status(200).json(results)) - .catch(handleApiError); - })); - - v1routes.get('/jobs/:jobId/controller', (req, res) => { - const { jobId } = req.params; - - logger.trace(`GET /jobs/:jobId/controller endpoint has been called, job_id: ${jobId}`); - const handleApiError = handleError(res, logger, 500, `Could not get controller statistics for job: ${jobId}`); - - jobsService.getLatestExecutionId(jobId) - .then(exId => _controllerStats(exId)) - .then(results => res.status(200).json(results)) - .catch(handleApiError); + requestHandler(async () => { + const exId = await _getExIdFromRequest(req); + const result = await _changeWorkers(exId, query); + return `${result.workerNum} workers have been ${result.action} for execution: ${result.ex_id}`; + }); }); - v1routes.get('/jobs/:jobId/errors', (req, res) => { - const { jobId } = req.params; - const { size, from, sort } = getSearchOptions(req); + v1routes.get([ + '/jobs/:jobId/slicer', + '/jobs/:jobId/controller', + '/ex/:exId/slicer', + '/ex/:exId/controller' + ], (req, res) => { + const requestHandler = handleRequest(req, res, 'Could not get slicer statistics'); - logger.trace(`GET /jobs/:jobId/errors endpoint has been called, job_id: ${jobId}, from: ${from}, size: ${size}`); - const handleApiError = handleError(res, logger, 500, `Could not get errors for job: ${jobId}`); - - jobsService.getLatestExecutionId(jobId) - .then((exId) => { - if (!exId) { - const error = new Error(`no executions were found for job: ${jobId}`); - error.code = 404; - return Promise.reject(error); - } - const query = `state:error AND ex_id:${exId}`; - return stateStore.search(query, from, size, sort); - }) - .then(errorStates => res.status(200).json(errorStates)) - .catch(handleApiError); + requestHandler(async () => { + const exId = await _getExIdFromRequest(req); + return _controllerStats(exId); + }); }); - v1routes.get('/jobs/:jobId/errors/:exId', (req, res) => { - const { jobId, exId } = req.params; + v1routes.get([ + '/jobs/:jobId/errors', + '/jobs/:jobId/errors/:exId', + '/ex/:exId/errors', + '/ex/errors', + ], (req, res) => { const { size, from, sort } = getSearchOptions(req); - logger.trace(`GET /jobs/:jobId/errors endpoint has been called, job_id: ${jobId}, ex_id: ${exId}, from: ${from}, size: ${size}`); - const handleApiError = handleError(res, logger, 500, `Could not get errors for job: ${jobId}, execution: ${exId}`); + const requestHandler = handleRequest(req, res, 'Could not get errors for job'); - const query = `ex_id:${exId} AND state:error`; + requestHandler(async () => { + const exId = await _getExIdFromRequest(req, true); - stateStore.search(query, from, size, sort) - .then((errorStates) => { - res.status(200).json(errorStates); - }) - .catch(handleApiError); + const query = `state:error AND ex_id:${exId}`; + return stateStore.search(query, from, size, sort); + }); }); v1routes.get('/ex', (req, res) => { const { status = '' } = req.query; const { size, from, sort } = getSearchOptions(req); - logger.trace(`GET /ex endpoint has been called, status: ${status}, from: ${from}, size: ${size}, sort: ${sort}`); - const handleApiError = handleError(res, logger, 500, 'Could not retrieve list of execution contexts'); - - const statuses = status.split(',').map(s => s.trim()).filter(s => !!s); - - let query = 'ex_id:*'; - - if (statuses.length) { - const statusTerms = statuses.map(s => `_status:${s}`).join(' OR '); - query += ` AND (${statusTerms})`; - } - - executionService.searchExecutionContexts(query, from, size, sort) - .then(results => res.status(200).json(results)) - .catch(handleApiError); - }); + const requestHandler = handleRequest(req, res, 'Could not retrieve list of execution contexts'); - v1routes.get('/ex/errors', (req, res) => { - const { size, from, sort } = getSearchOptions(req); + requestHandler(async () => { + const statuses = status.split(',').map(s => s.trim()).filter(s => !!s); - logger.trace(`GET /ex/errors endpoint has been called, from: ${from}, size: ${size}`); - const handleApiError = handleError(res, logger, 500, 'Could not get errors'); + let query = 'ex_id:*'; - const query = 'ex_id:* AND state:error'; + if (statuses.length) { + const statusTerms = statuses.map(s => `_status:${s}`).join(' OR '); + query += ` AND (${statusTerms})`; + } - stateStore.search(query, from, size, sort) - .then(errorStates => res.status(200).json(errorStates)) - .catch(handleApiError); + return executionService.searchExecutionContexts(query, from, size, sort); + }); }); v1routes.get('/ex/:exId', (req, res) => { const { exId } = req.params; - logger.trace(`GET /ex/:exId endpoint has been called, ex_id: ${exId}`); - const handleApiError = handleError(res, logger, 500, `Could not retrieve execution context ${exId}`); - - executionService.getExecutionContext(exId) - .then(results => res.status(200).json(results)) - .catch(handleApiError); - }); - - v1routes.get('/ex/:exId/errors', (req, res) => { - const { exId } = req.params; - const { size, from, sort } = getSearchOptions(req); - - logger.trace(`GET /ex/:exId/errors endpoint has been called, ex_id: ${exId}, from: ${from}, size: ${size}`); - const handleApiError = handleError(res, logger, 500, `Could not get errors for ex_id ${exId}`); - - const query = `ex_id:${exId} AND state:error`; + const requestHandler = handleRequest(req, res, `Could not retrieve execution context ${exId}`); - stateStore.search(query, from, size, sort) - .then(errorStates => res.status(200).json(errorStates)) - .catch(handleApiError); - }); - - v1routes.get('/ex/:exId/slicer', _deprecateSlicerName((req, res) => { - const { exId } = req.params; - - logger.trace(`GET /ex/:exId/slicer endpoint has been called, ex_id: ${exId}`); - const handleApiError = handleError(res, logger, 500, `Could not get statistics for execution: ${exId}`); - - _controllerStats(exId) - .then(results => res.status(200).json(results)) - .catch(handleApiError); - })); - - v1routes.get('/ex/:exId/controller', (req, res) => { - const { exId } = req.params; - - logger.trace(`GET /ex/:exId/controller endpoint has been called, ex_id: ${exId}`); - const handleApiError = handleError(res, logger, 500, `Could not get statistics for execution: ${exId}`); - - _controllerStats(exId) - .then(results => res.status(200).json(results)) - .catch(handleApiError); - }); - - - v1routes.post('/ex/:exId/_stop', (req, res) => { - const { exId } = req.params; - const { timeout, blocking = true } = req.query; - - logger.trace(`POST /ex/:exId/_stop endpoint has been called, ex_id: ${exId}, removing any pending workers for the job`); - const handleApiError = handleError(res, logger, 500, `Could not stop execution: ${exId}`); - - executionService.stopExecution(exId, timeout) - .then(() => _waitForStop(exId, blocking)) - .then(status => res.status(200).json({ status })) - .catch(handleApiError); - }); - - v1routes.post('/ex/:exId/_pause', (req, res) => { - const { exId } = req.params; - - logger.trace(`POST /ex/:exId/_pause endpoint has been called, ex_id: ${exId}`); - const handleApiError = handleError(res, logger, 500, `Could not pause execution: ${exId}`); - - // for lifecyle events, we need to ensure that the execution is alive first - executionService.getActiveExecution(exId) - .then(() => executionService.pauseExecution(exId)) - .then(() => res.status(200).json({ status: 'paused' })) - .catch(handleApiError); - }); - - v1routes.post('/ex/:exId/_recover', (req, res) => { - const { exId } = req.params; - const { cleanup } = req.query; - - const handleApiError = handleError(res, logger, 500, `Could not recover execution: ${exId}`); - logger.trace(`POST /ex/:exId/_recover endpoint has been called, ex_id: ${exId}`); - - if (cleanup && !(cleanup === 'all' || cleanup === 'errors')) { - res.status(400).json({ - error: 'if cleanup is specified it must be set to "all" or "errors"' - }); - return; - } - - executionService.recoverExecution(exId, cleanup) - .then(response => res.status(200).json(response)) - .catch(handleApiError); - }); - - v1routes.post('/ex/:exId/_resume', (req, res) => { - const { exId } = req.params; - - logger.trace(`POST /ex/:id/_resume endpoint has been called, ex_id: ${exId}`); - const handleApiError = handleError(res, logger, 500, `Could not resume execution: ${exId}`); - - // for lifecyle events, we need to ensure that the execution is alive first - executionService.getActiveExecution(exId) - .then(() => executionService.resumeExecution(exId)) - .then(() => res.status(200).json({ status: 'resumed' })) - .catch(handleApiError); - }); - - v1routes.post('/ex/:exId/_workers', (req, res) => { - const { exId } = req.params; - const { query } = req; - - logger.trace(`POST /ex/:id/_workers endpoint has been called, ex_id: ${exId} query: ${JSON.stringify(query)}`); - const handleApiError = handleError(res, logger, 500, `Could not change workers for execution: ${exId}`); - - _changeWorkers('execution', exId, query) - .then(responseObj => res.status(200).send(`${responseObj.workerNum} workers have been ${responseObj.action} for execution: ${responseObj.ex_id}`)) - .catch(handleApiError); + requestHandler(async () => executionService.getExecutionContext(exId)); }); v1routes.get('/cluster/stats', (req, res) => { - logger.trace('GET /cluster/stats endpoint has been called'); - - const stats = executionService.getClusterStats(); const { name: cluster } = context.sysconfig.teraslice; - if (isPrometheusRequest(req)) { - res.status(200).send(makePrometheus(stats, { cluster })); - } else { + const requestHandler = handleRequest(req, res, 'Could not get cluster statistics'); + + requestHandler(() => { + const stats = executionService.getClusterStats(); + + if (isPrometheusRequest(req)) return makePrometheus(stats, { cluster }); // for backwards compatability (unsupported for prometheus) stats.slicer = stats.controllers; - res.status(200).json(stats); - } + return stats; + }); }); - v1routes.get('/cluster/slicers', _deprecateSlicerName((req, res) => { - logger.trace('GET /cluster/slicers endpoint has been called'); - const handleApiError = handleError(res, logger, 500, 'Could not get execution statistics'); + v1routes.get(['/cluster/slicers', '/cluster/controllers'], (req, res) => { + const requestHandler = handleRequest(req, res, 'Could not get execution statistics'); - _controllerStats() - .then(results => res.status(200).send(results)) - .catch(handleApiError); - })); - - v1routes.get('/cluster/controllers', (req, res) => { - logger.trace('GET /cluster/controllers endpoint has been called'); - const handleApiError = handleError(res, logger, 500, 'Could not get execution statistics'); - - _controllerStats() - .then(results => res.status(200).send(results)) - .catch(handleApiError); + requestHandler(() => _controllerStats()); }); // backwards compatibility for /v1 routes @@ -471,87 +286,60 @@ module.exports = function module(context, app, { assetsUrl }) { .get(_redirect); app.get('/txt/workers', (req, res) => { - logger.trace('GET /txt/workers endpoint has been called'); - const defaults = ['assignment', 'job_id', 'ex_id', 'node_id', 'pid']; - const workers = executionService.findAllWorkers(); - const tableStr = makeTable(req, defaults, workers); - res.status(200).send(tableStr); + const requestHandler = handleRequest(req, res, 'Could not get all workers'); + + requestHandler(async () => { + const workers = await executionService.findAllWorkers(); + return makeTable(req, defaults, workers); + }); }); app.get('/txt/nodes', (req, res) => { - logger.trace('GET /txt/nodes endpoint has been called'); - const defaults = ['node_id', 'state', 'hostname', 'total', 'active', 'pid', 'teraslice_version', 'node_version']; - const nodes = executionService.getClusterState(); + const requestHandler = handleRequest(req, res, 'Could not get all nodes'); - const transform = _.map(nodes, (node) => { - node.active = node.active.length; - return node; - }); + requestHandler(async () => { + const nodes = await executionService.getClusterState(); - const tableStr = makeTable(req, defaults, transform); - res.status(200).send(tableStr); + const transform = _.map(nodes, (node) => { + node.active = node.active.length; + return node; + }); + + return makeTable(req, defaults, transform); + }); }); app.get('/txt/jobs', (req, res) => { const { size, from, sort } = getSearchOptions(req); - logger.trace('GET /txt/jobs endpoint has been called'); - const handleApiError = handleError(res, logger, 500, 'Could not get all jobs'); + const requestHandler = handleRequest(req, res, 'Could not get all jobs'); const defaults = ['job_id', 'name', 'lifecycle', 'slicers', 'workers', '_created', '_updated']; - jobsService.getJobs(from, size, sort) - .then((jobs) => { - const tableStr = makeTable(req, defaults, jobs); - res.status(200).send(tableStr); - }) - .catch(handleApiError); + requestHandler(async () => { + const jobs = await jobsService.getJobs(from, size, sort); + return makeTable(req, defaults, jobs); + }); }); app.get('/txt/ex', (req, res) => { const { size, from, sort } = getSearchOptions(req); - logger.trace('GET /txt/ex endpoint has been called'); - const handleApiError = handleError(res, logger, 500, 'Could not get all executions'); + const requestHandler = handleRequest(req, res, 'Could not get all executions'); const defaults = ['name', 'lifecycle', 'slicers', 'workers', '_status', 'ex_id', 'job_id', '_created', '_updated']; const query = 'ex_id:*'; - executionService.searchExecutionContexts(query, from, size, sort) - .then((jobs) => { - const tableStr = makeTable(req, defaults, jobs); - res.status(200).send(tableStr); - }) - .catch(handleApiError); + requestHandler(async () => { + const exs = await executionService.searchExecutionContexts(query, from, size, sort); + return makeTable(req, defaults, exs); + }); }); - app.get('/txt/slicers', _deprecateSlicerName((req, res) => { - logger.trace('GET /txt/slicers endpoint has been called'); - const handleApiError = handleError(res, logger, 500, 'Could not get all execution statistics'); - - const defaults = [ - 'name', - 'job_id', - 'workers_available', - 'workers_active', - 'failed', - 'queued', - 'processed' - ]; - - _controllerStats() - .then((results) => { - const tableStr = makeTable(req, defaults, results); - res.status(200).send(tableStr); - }) - .catch(handleApiError); - })); - - app.get('/txt/controllers', (req, res) => { - logger.trace('GET /txt/controllers endpoint has been called'); - const handleApiError = handleError(res, logger, 500, 'Could not get all execution statistics'); + app.get(['/txt/slicers', '/txt/controllers'], (req, res) => { + const requestHandler = handleRequest(req, res, 'Could not get all execution statistics'); const defaults = [ 'name', @@ -563,12 +351,10 @@ module.exports = function module(context, app, { assetsUrl }) { 'processed' ]; - _controllerStats() - .then((results) => { - const tableStr = makeTable(req, defaults, results); - res.status(200).send(tableStr); - }) - .catch(handleApiError); + requestHandler(async () => { + const stats = await _controllerStats(); + return makeTable(req, defaults, stats); + }); }); // This is a catch all, any none supported api endpoints will return an error @@ -577,8 +363,7 @@ module.exports = function module(context, app, { assetsUrl }) { sendError(res, 405, `cannot ${req.method} endpoint ${req.originalUrl}`); }); - function _changeWorkers(type, id, query) { - const serviceContext = type === 'job' ? jobsService : executionService; + function _changeWorkers(exId, query) { let msg; let workerNum; const keyOptions = { add: true, remove: true, total: true }; @@ -589,6 +374,7 @@ module.exports = function module(context, app, { assetsUrl }) { error.code = 400; return Promise.reject(error); } + queryKeys.forEach((key) => { if (keyOptions[key]) { msg = key; @@ -603,19 +389,44 @@ module.exports = function module(context, app, { assetsUrl }) { } if (msg === 'add') { - return serviceContext.addWorkers(id, workerNum); + return executionService.addWorkers(exId, workerNum); } if (msg === 'remove') { - return serviceContext.removeWorkers(id, workerNum); + return executionService.removeWorkers(exId, workerNum); } - return serviceContext.setWorkers(id, workerNum); + return executionService.setWorkers(exId, workerNum); } - function _deprecateSlicerName(fn) { - const msg = 'api endpoints with /slicers are being deprecated in favor of the semantically correct term of /controllers'; - return util.deprecate(fn, msg); + async function _getExIdFromRequest(req, allowWildcard = false) { + const { path } = req; + if (_.startsWith(path, '/ex')) { + const { exId } = req.params; + if (exId) return exId; + + if (allowWildcard) { + return '*'; + } + const error = new Error('Execution Context ID is required'); + error.code = 406; + throw error; + } + + if (_.startsWith(path, '/jobs')) { + const { jobId } = req.params; + const exId = await jobsService.getLatestExecutionId(jobId); + if (!exId) { + const error = new Error(`No executions were found for job: ${jobId}`); + error.code = 404; + throw error; + } + return exId; + } + + const error = new Error('Only /ex and /jobs are allowed'); + error.code = 405; + throw error; } function _redirect(req, res) { @@ -638,14 +449,6 @@ module.exports = function module(context, app, { assetsUrl }) { return Promise.resolve(true); } - const api = { - shutdown - }; - - function _initialize() { - return Promise.resolve(api); - } - function _waitForStop(exId, blocking) { return new Promise((resolve) => { function checkExecution() { @@ -653,8 +456,11 @@ module.exports = function module(context, app, { assetsUrl }) { .then((execution) => { const terminalList = executionService.terminalStatusList(); const isTerminal = terminalList.find(tStat => tStat === execution._status); - if (isTerminal || !(blocking === true || blocking === 'true')) resolve(execution._status); - else setTimeout(checkExecution, 3000); + if (isTerminal || !(blocking === true || blocking === 'true')) { + resolve({ status: execution._status }); + } else { + setTimeout(checkExecution, 3000); + } }) .catch((err) => { logger.error(err); @@ -666,10 +472,9 @@ module.exports = function module(context, app, { assetsUrl }) { }); } - return require('../storage/state')(context) - .then((state) => { - logger.info('api service is initializing...'); - stateStore = state; - return _initialize(); // Load the initial pendingJobs state. - }); + logger.info('api service is initializing...'); + + return { + shutdown, + }; }; diff --git a/packages/teraslice/lib/cluster/services/assets.js b/packages/teraslice/lib/cluster/services/assets.js index c955f7fff2e..b22ee379d1b 100644 --- a/packages/teraslice/lib/cluster/services/assets.js +++ b/packages/teraslice/lib/cluster/services/assets.js @@ -5,7 +5,7 @@ const _ = require('lodash'); const express = require('express'); const parseError = require('@terascope/error-parser'); const makeAssetsStore = require('../storage/assets'); -const { makeTable, handleError } = require('../../utils/api_utils'); +const { makeTable, handleRequest, getSearchOptions } = require('../../utils/api_utils'); module.exports = function module(context) { const logger = context.apis.foundation.makeLogger({ module: 'assets_service' }); @@ -14,8 +14,16 @@ module.exports = function module(context) { let assetsStore; let running = false; + app.set('json spaces', 4); + + app.use((req, res, next) => { + req.logger = logger; + next(); + }); + app.get('/status', (req, res) => { - res.send({ available: running }); + const requestHandler = handleRequest(req, res); + requestHandler(() => ({ available: running })); }); app.post('/assets', (req, res) => { @@ -44,18 +52,17 @@ module.exports = function module(context) { }); }); - app.delete('/assets/:asset_id', (req, res) => { - const assetId = req.params.asset_id; - const handleApiError = handleError(res, logger, 500, `Could not delete asset ${assetId}`); + app.delete('/assets/:assetId', (req, res) => { + const { assetId } = req.params; + const requestHandler = handleRequest(req, res, `Could not delete asset ${assetId}`); if (assetId.length !== 40) { res.status(400).json({ error: `asset ${assetId} is not formatted correctly, please provide the full asset_id` }); } else { - assetsStore.remove(assetId) - .then(() => { - res.status(200).json({ assetId }); - }) - .catch(handleApiError); + requestHandler(async () => { + await assetsStore.remove(assetId); + return { assetId }; + }); } }); @@ -76,23 +83,22 @@ module.exports = function module(context) { app.get('/assets', (req, res) => { const query = 'id:*'; - assetsSearch(query, req, res) - .then(results => res.status(200).send(JSON.stringify(results, null, 4))); + assetsSearch(query, req, res); }); app.get('/assets/:name', (req, res) => { const query = `id:* AND name:${req.params.name}`; - assetsSearch(query, req, res) - .then(results => res.status(200).send(JSON.stringify(results, null, 4))); + assetsSearch(query, req, res); }); app.get('/assets/:name/:version', (req, res) => { const query = `id:* AND name:${req.params.name} AND version:${req.params.version}`; - assetsSearch(query, req, res) - .then(results => res.status(200).send(JSON.stringify(results, null, 4))); + assetsSearch(query, req, res); }); function createAssetTable(query, req, res) { + const { size, from, sort } = getSearchOptions(req, '_created:desc'); + const defaults = [ 'name', 'version', @@ -110,26 +116,34 @@ module.exports = function module(context) { }; } - assetsSearch(query, res, res) - .then((queryResults) => { - const tableStr = makeTable(req, defaults, queryResults, mapping); - res.status(200).send(tableStr); + const requestHandler = handleRequest(req, res, 'Could not get assets'); + + requestHandler(async () => { + const results = await assetsStore.search(query, from, size, sort, defaults); + const data = results.hits.hits; + const assets = _.map(data, (asset) => { + const record = asset._source; + record.id = asset._id; + return record; }); + return makeTable(req, defaults, assets, mapping); + }); } function assetsSearch(query, req, res) { - const handleApiError = handleError(res, logger, 500, 'Could not get assets'); - - return assetsStore.search(query, null, 10000, '_created:desc', ['_created', 'name', 'version', 'description']) - .then((results) => { - const data = results.hits.hits; - return _.map(data, (asset) => { - const record = asset._source; - record.id = asset._id; - return record; - }); - }) - .catch(handleApiError); + const { size, from, sort } = getSearchOptions(req, '_created:desc'); + const requestHandler = handleRequest(req, res, 'Could not get assets'); + + requestHandler(async () => { + const fields = ['_created', 'name', 'version', 'description']; + const results = await assetsStore.search(query, from, size, sort, fields); + const data = results.hits.hits; + return _.map(data, (asset) => { + const record = asset._source; + record.id = asset._id; + return record; + }); + }); } diff --git a/packages/teraslice/lib/processors/stdout/processor.js b/packages/teraslice/lib/processors/stdout/processor.js index 21e020ad4f4..6e8da39b40b 100644 --- a/packages/teraslice/lib/processors/stdout/processor.js +++ b/packages/teraslice/lib/processors/stdout/processor.js @@ -1,13 +1,16 @@ 'use strict'; +/* eslint-disable no-console */ + +const _ = require('lodash'); const { BatchProcessor } = require('@terascope/job-components'); class Stdout extends BatchProcessor { async onBatch(data) { if (this.opConfig.limit === 0) { - console.log(data); // eslint-disable-line + console.log(data); } else { - console.log(_.take(data, opConfig.limit)); // eslint-disable-line + console.log(_.take(data, this.opConfig.limit)); } return data; } diff --git a/packages/teraslice/lib/utils/api_utils.js b/packages/teraslice/lib/utils/api_utils.js index 6ee171d706b..7350fce82cc 100644 --- a/packages/teraslice/lib/utils/api_utils.js +++ b/packages/teraslice/lib/utils/api_utils.js @@ -7,13 +7,16 @@ const parseError = require('@terascope/error-parser'); function makeTable(req, defaults, data, mappingFn) { const query = fieldsQuery(req.query, defaults); + let emptyChar = 'N/A'; + // used to create an empty table if there are no jobs if (data.length === 0) { + emptyChar = ''; data.push({}); } return Table.print(data, (item, cell) => { - const fn = mappingFn ? mappingFn(item) : field => item[field] || 'N/A'; + const fn = mappingFn ? mappingFn(item) : field => item[field] || emptyChar; _.each(query, (field) => { cell(field, fn(field)); }); @@ -39,17 +42,28 @@ function fieldsQuery(query, defaults) { return results; } -function handleError(res, logger, defualtCode, defaultErrorMsg) { - return (errObj) => { - if (_.isError(errObj) || errObj.code || errObj.statusCode) { - const code = errObj.statusCode || errObj.code || 500; - logger.error(errObj.message); - sendError(res, code, errObj.message); - return; +function handleRequest(req, res, defaultErrorMsg = 'Failure to process request', { errorCode = 500, successCode = 200 } = {}) { + logRequest(req); + return async (fn) => { + try { + const result = await fn(); + if (_.isString(result)) { + res.status(successCode).send(result); + } else { + res.status(successCode).json(result); + } + } catch (err) { + if (_.isError(err) || err.code || err.statusCode) { + const code = err.statusCode || err.code || errorCode; + req.logger.error(err.message); + sendError(res, code, err.message); + return; + } + + const errMsg = `${defaultErrorMsg}, error: ${parseError(err)}`; + req.logger.error(errMsg); + sendError(res, errorCode, errMsg); } - const errMsg = `${defaultErrorMsg}, error: ${parseError(errObj)}`; - logger.error(errMsg); - sendError(res, defualtCode, errMsg); }; } @@ -103,16 +117,23 @@ function isPrometheusRequest(req) { return acceptHeader && acceptHeader.indexOf('application/openmetrics-text;') > -1; } -function getSearchOptions(req) { - const { size = 1000, from, sort = '_updated:asc' } = req.query; +function getSearchOptions(req, defaultSort = '_updated:asc') { + const { size = 100, from = null, sort = defaultSort } = req.query; return { size, from, sort }; } +function logRequest(req) { + const queryInfo = _.map(req.query, (val, key) => `${key}: ${val}`).join(', '); + const { method, path } = req; + req.logger.trace(`${_.toUpper(method)} ${path} endpoint has been called, ${queryInfo}`); +} + module.exports = { isPrometheusRequest, makePrometheus, makeTable, + logRequest, getSearchOptions, - handleError, + handleRequest, sendError }; diff --git a/packages/teraslice/package.json b/packages/teraslice/package.json index 02360090e07..964e6b0695a 100644 --- a/packages/teraslice/package.json +++ b/packages/teraslice/package.json @@ -74,6 +74,7 @@ "eslint": "^5.8.0", "eslint-config-airbnb-base": "^13.1.0", "eslint-plugin-import": "^2.14.0", + "got": "^9.3.1", "jest": "^23.6.0", "jest-extended": "^0.11.0", "jest-fixtures": "^0.6.0", diff --git a/packages/teraslice/test/services/api-spec.js b/packages/teraslice/test/services/api-spec.js new file mode 100644 index 00000000000..08351ff2c2c --- /dev/null +++ b/packages/teraslice/test/services/api-spec.js @@ -0,0 +1,81 @@ +'use strict'; + +const got = require('got'); +const express = require('express'); +const { TestContext } = require('@terascope/job-components'); +const { version } = require('../../package.json'); +const { findPort } = require('../../lib/utils/port_utils'); +const makeAPI = require('../../lib/cluster/services/api'); + +describe('HTTP API', () => { + const app = express(); + const assetsUrl = 'http://example.asset:1234'; + const context = new TestContext('http-api'); + context.services = { + execution: { + + }, + jobs: { + + } + }; + + const stateStore = { + + }; + + let api; + let port; + let baseUrl; + let server; + + beforeAll(async () => { + port = await findPort(); + + baseUrl = `http://localhost:${port}`; + + api = await makeAPI(context, app, { assetsUrl, stateStore }); + + await new Promise((resolve, reject) => { + server = app.listen(port, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + }); + + afterAll(async () => { + if (api) { + await api.shutdown(); + } + if (server) { + await new Promise((resolve, reject) => { + server.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + }); + + describe('GET /', () => { + it('should the correct response', async () => { + let response; + + try { + response = await got('/', { baseUrl, json: true }); + } catch (err) { + expect(err.stack).toBeNil(); + } + + expect(response.body).toMatchObject({ + arch: context.arch, + clustering_type: context.sysconfig.teraslice.cluster_manager_type, + name: context.sysconfig.teraslice.name, + node_version: process.version, + platform: context.platform, + teraslice_version: `v${version}` + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index b84fe6bff51..20679404efe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -621,11 +621,25 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.2.tgz#54c5a964462be3d4d78af631363c18d6fa91ac26" integrity sha512-yprFYuno9FtNsSHVlSWd+nRlmGoAbqbeCwOryP6sC/zoCjhpArcRMYp19EvpSUSizJAlsXEwJv+wcWS9XaXdMw== +"@sindresorhus/is@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.12.0.tgz#55c37409c809e802efea25911a579731adfc6e07" + integrity sha512-9ve22cGrAKlSRvi8Vb2JIjzcaaQg79531yQHnF+hi/kOpsSj3Om8AyR1wcHrgl0u7U3vYQ7gmF5erZzOp4+51Q== + dependencies: + symbol-observable "^1.2.0" + "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@szmarczak/http-timer@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.1.tgz#6402258dfe467532b26649ef076b4d11f74fb612" + integrity sha512-WljfOGkmSJe8SUkl+4TPvN2ec0dpUGVyfTBQLoXJUiILs+wBSc4Kvp2N3aAWE4VwwDSLGdmD3/bufS5BgZpVSQ== + dependencies: + defer-to-connect "^1.0.1" + "@terascope/fetch-github-release@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@terascope/fetch-github-release/-/fetch-github-release-0.4.1.tgz#526de62b1f6a30802eb7db338caa7f216c0d0a29" @@ -1652,6 +1666,19 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +cacheable-request@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-5.1.0.tgz#ce0958e977bdb4a5b718464049793b8d4bf7d75d" + integrity sha512-UCdjX4N/QjymZGpKY7hW4VJsxsVJM+drIiCxPa9aTvFQN5sL2+kJCYyeys8f2W0dJ0sU6Et54Ovl0sAmCpHHsA== + dependencies: + clone-response "^1.0.2" + get-stream "^4.0.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^1.0.1" + normalize-url "^3.1.0" + responselike "^1.0.2" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -1852,7 +1879,7 @@ cliui@^4.0.0: strip-ansi "^4.0.0" wrap-ansi "^2.0.0" -clone-response@1.0.2: +clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -2486,6 +2513,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.0.1.tgz#41ec1dd670dc4c6dcbe7e54c9e44d784d025fe63" + integrity sha512-2e0FJesseUqQj671gvZWfUyxpnFx/5n4xleamlpCD3U6Fm5dh5qzmmLNxNhtmHF06+SYVHH8QU6FACffYTnj0Q== + define-properties@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -3825,6 +3857,23 @@ got@^8.3.2: url-parse-lax "^3.0.0" url-to-options "^1.0.1" +got@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/got/-/got-9.3.1.tgz#f0e6d531e6ece50375de4f18a640b716862f102c" + integrity sha512-lIVTDQipyF6uz7FpfHJ2Yd67tgb58C6eKAE46CYPqPj4BEkCT/HgdeRx8nF64BbVJWasEtlEJ3OBziuwl8tYrA== + dependencies: + "@sindresorhus/is" "^0.12.0" + "@szmarczak/http-timer" "^1.1.0" + cacheable-request "^5.1.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + graceful-fs@^4.1.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -3988,6 +4037,11 @@ http-cache-semantics@3.8.1, http-cache-semantics@^3.8.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +http-cache-semantics@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#2d0069a73c36c80e3297bc3a0cadd669b78a69ce" + integrity sha512-NtexGRtaV5z3ZUX78W9UDTOJPBdpqms6RmwQXmOhHws7CuQK3cqIoQtnmeqi1VvVD6u6eMMRL0sKE9BCZXTDWQ== + http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" @@ -5214,6 +5268,13 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -5553,7 +5614,7 @@ lowercase-keys@1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= -lowercase-keys@^1.0.0: +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== @@ -5790,7 +5851,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-response@^1.0.0: +mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -6292,6 +6353,11 @@ normalize-url@2.0.1: query-string "^5.0.1" sort-keys "^2.0.0" +normalize-url@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + npm-bundled@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" @@ -6587,6 +6653,11 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== +p-cancelable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.0.0.tgz#07e9c6d22c31f9c6784cb4f1e1454a79b6d9e2d6" + integrity sha512-USgPoaC6tkTGlS831CxsVdmZmyb8tR1D+hStI84MyckLOzfJlYQUweomrwE3D8T7u5u5GVuW064LT501wHTYYA== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -7576,7 +7647,7 @@ resolve@^1.3.2, resolve@^1.5.0, resolve@^1.6.0: dependencies: path-parse "^1.0.5" -responselike@1.0.2: +responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= @@ -8298,6 +8369,11 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" @@ -8490,6 +8566,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"