From 70e9ec959348d5544afbca19a4378130325771ed Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Thu, 21 Sep 2017 11:59:54 -0400 Subject: [PATCH 01/13] updated to use querystring for create/delete eventSubscriptions --- lib/subscriptions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/subscriptions.js b/lib/subscriptions.js index d61da71..79da4e9 100644 --- a/lib/subscriptions.js +++ b/lib/subscriptions.js @@ -3,10 +3,10 @@ module.exports = function createMethods(makeRequest) { // /v2/eventSubscriptions getList: makeRequest('GET', '/eventSubscriptions'), create: function create(callbackUrl) { - return makeRequest('POST', '/eventSubscriptions')({callbackUrl: callbackUrl}); + return makeRequest('POST', '/eventSubscriptions')({qs: "callbackUrl=" + callbackUrl}); }, delete: function deleteSubscription(callbackUrl) { - return makeRequest('DELETE', '/eventSubscriptions')({callbackUrl: callbackUrl}); + return makeRequest('DELETE', '/eventSubscriptions')({qs: "callbackUrl=" + callbackUrl}); } }; }; From 27c9f156eae103e5ac2fbfe4ffe30052bc9ec8f4 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Thu, 18 Jan 2018 14:54:52 -0500 Subject: [PATCH 02/13] updated lib/events.js to handle authentication EventSource does not use an `auth` option and needs user/pass in the url if you are authenticating marathon operator requests. --- lib/events.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/events.js b/lib/events.js index 57a8f65..d1da2b2 100644 --- a/lib/events.js +++ b/lib/events.js @@ -16,7 +16,13 @@ module.exports = function createMethods(makeRequest, url) { opts = opts || {}; url.query = url.query || {}; - + // Set username/password in url for eventsource requests + if (!url.auth && opts.auth.length >= 2) { + if (typeof opts.auth.user != "undefined" && + typeof opts.auth.pass != "undefined" ) { + url.auth = opts.auth.user + ":" + opts.auth.pass; + } + } var eventType = opts.eventType; if (eventType) { if (!_.isString(eventType) && !_.isArray(eventType)) { From 59f016887014cbfc2781bddc457b60b48fbcf611 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Thu, 18 Jan 2018 15:39:00 -0500 Subject: [PATCH 03/13] simplify authentication mechanism for lib/events --- lib/events.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/events.js b/lib/events.js index d1da2b2..f9fdbb8 100644 --- a/lib/events.js +++ b/lib/events.js @@ -14,15 +14,12 @@ module.exports = function createMethods(makeRequest, url) { createEventSource: function createEventSource(opts) { opts = opts || {}; - - url.query = url.query || {}; // Set username/password in url for eventsource requests - if (!url.auth && opts.auth.length >= 2) { - if (typeof opts.auth.user != "undefined" && - typeof opts.auth.pass != "undefined" ) { - url.auth = opts.auth.user + ":" + opts.auth.pass; - } + if ( "auth" in opts ) { + url.auth = opts.auth.user + ":" + opts.auth.pass; } + url.query = url.query || {}; + var eventType = opts.eventType; if (eventType) { if (!_.isString(eventType) && !_.isArray(eventType)) { From c9c6088f558c519ccb23bb2bc536a807d7256b53 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Thu, 23 Feb 2023 13:55:41 -0500 Subject: [PATCH 04/13] update to use native promises and axios client --- .gitignore | 1 + lib/apps.js | 126 +++++++++++++++++++-------------- lib/deployments.js | 34 +++++---- lib/events.js | 51 +++++++------- lib/groups.js | 56 +++++++++------ lib/info.js | 25 ++++--- lib/leader.js | 32 ++++++--- lib/marathon.js | 172 ++++++++++++++++++--------------------------- lib/misc.js | 31 +++++--- lib/nodash.js | 99 ++++++++++++++++++++++++++ lib/queue.js | 33 +++++---- lib/tasks.js | 34 +++++---- package.json | 17 ++--- postpublish.js | 30 ++++---- 14 files changed, 451 insertions(+), 290 deletions(-) create mode 100644 lib/nodash.js diff --git a/.gitignore b/.gitignore index 3c3629e..fc5952d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +*.sublime* diff --git a/lib/apps.js b/lib/apps.js index a941a9f..1139b4e 100644 --- a/lib/apps.js +++ b/lib/apps.js @@ -1,51 +1,75 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/apps - getList: function getList(query) { - return makeRequest('GET', '/apps')(query); - }, - create: function create(body) { - return makeRequest('POST', '/apps')(null, body); - }, - - // /v2/apps/{app_id} - getOne: function getOne(appId, query) { - return makeRequest('GET', '/apps/' + appId)(query); - }, - update: function update(appId, body, force) { - return makeRequest('PUT', '/apps/' + appId)({force: force}, body); - }, - destroy: function destroy(appId, force) { - return makeRequest('DELETE', '/apps/' + appId)({force: force}); - }, - - // /v2/apps/{app_id}/restart - restart: function restart(appId, force) { - return makeRequest('POST', '/apps/' + appId + '/restart')({force: force}); - }, - - // /v2/apps/{app_id}/tasks - getTasks: function getTasks(appId) { - return makeRequest('GET', '/apps/' + appId + '/tasks')(); - }, - - killTasks: function killTasks(appId, parameters) { - return makeRequest('DELETE', '/apps/' + appId + '/tasks')(parameters); - }, - - // /v2/apps/{app_id}/tasks/{task_id} - killTask: function killTask(appId, taskId, scale) { - return makeRequest('DELETE', '/apps/' + appId + '/tasks/' + taskId)({scale: scale}); - }, - - // /v2/apps/{app_id}/versions - getVersions: function getVersions(appId) { - return makeRequest('GET', '/apps/' + appId + '/versions')(); - }, - - // /v2/apps/{app_id}/versions/{version} - getVersion: function getVersion(appId, versionId) { - return makeRequest('GET', '/apps/' + appId + '/versions/' + versionId)(); - } - }; -}; +'use strict' + +class MarathonApiAppEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/apps` + this.client = ctx.http.create() + this.client.interceptors = ctx.http.interceptors + this.client.defaults.baseURL = this.baseURL.toString() + } + + async getList (query = {}) { + const params = new URLSearchParams(Object.entries(query)) + console.log({ clientDefaults: this.client.defaults }) + return this.client.get('', { params }) + } + + async create (data) { + return this.client.post('', data) + } + + async getOne (appId, query = {}) { + const params = new URLSearchParams(Object.entries(query)) + const path = encodeURIComponent(appId) + return this.client.get(path, { params }) + } + + async update (appId, data, force) { + const params = new URLSearchParams([['force', !!force]]) + const path = encodeURIComponent(appId) + return this.client.put(path, data, { params }) + } + + async destroy (appId, force) { + const params = new URLSearchParams([['force', !!force]]) + const path = encodeURIComponent(appId) + return this.client.delete(path, undefined, { params }) + } + + async restart (appId, force) { + const params = new URLSearchParams([['force', !!force]]) + const path = `${encodeURIComponent(appId)}/restart` + return this.client.post(path, undefined, { params }) + } + + async getTasks (appId) { + const path = `${encodeURIComponent(appId)}/tasks` + return this.client.get(`${path}/tasks`) + } + + async killTasks (appId, query = {}) { + const params = new URLSearchParams(Object.entries(query)) + const path = `${encodeURIComponent(appId)}/tasks` + return this.client.delete(path, undefined, { params }) + } + + async killTask (appId, taskId, scale) { + const params = new URLSearchParams([['scale', !!scale]]) + const path = `${encodeURIComponent(appId)}/tasks/${taskId}` + return this.client.delete(path, undefined, { params }) + } + + async getVersions (appId) { + const path = `${encodeURIComponent(appId)}/versions` + return this.client.get(path) + } + + async getVersion (appId, versionId) { + const path = `${encodeURIComponent(appId)}/versions/${versionId}` + return this.client.get(path) + } +} + +module.exports = MarathonApiAppEndpoints diff --git a/lib/deployments.js b/lib/deployments.js index bf3de2b..a6de192 100644 --- a/lib/deployments.js +++ b/lib/deployments.js @@ -1,12 +1,22 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/deployments - getList: function getList() { - return makeRequest('GET', '/deployments')(); - }, - // /v2/deployments/{deployment_id} - destroy: function destroy(deploymentId, force) { - return makeRequest('DELETE', '/deployments/' + deploymentId)({force: force}); - } - }; -}; +'use strict' + +class MarathonApiDeploymentEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/deployments` + this.client = ctx.http.create() + this.client.defaults.baseURL = this.baseURL.toString() + } + + async getList () { + return this.client.get() + } + + async destroy (deploymentId, force) { + const params = new URLSearchParams([['force', force]]) + return this.client.delete(deploymentId, { params }) + } +} + +module.exports = MarathonApiDeploymentEndpoints diff --git a/lib/events.js b/lib/events.js index 57a8f65..716fee9 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1,31 +1,28 @@ -'use strict'; +'use strict' -var util = require('util'); -var EventSource = require('eventsource'); -var nodeUrl = require('url'); -var _ = require('lodash'); +var EventSource = require('eventsource') -module.exports = function createMethods(makeRequest, url) { - return { - // /v2/events - attach: util.deprecate(function attach() { - return makeRequest('GET', '/events', {headers: {Accept: 'text/event-stream'}}, true)(); - }, '"events.attach()" is deprecated in favor of "events.createEventSource()"'), +class MarathonEventSource { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/events` + this.client = ctx.http.create() + this.client.defaults.baseURL = this.baseURL.toString() + } - createEventSource: function createEventSource(opts) { - opts = opts || {}; + // the constructor url should contain credentials and the api basepath '/v2' + createEventSource (opts = {}) { + const { eventType } = opts + if (eventType) { + if (!((eventType instanceof String) || (eventType instanceof Array))) { + throw new Error('"eventType" should be an array or string') + } + this.baseURL.searchParams.set('event_type', eventType) + } + this.es = new EventSource(`${this.baseURL}`) + return this.es + } +} - url.query = url.query || {}; - - var eventType = opts.eventType; - if (eventType) { - if (!_.isString(eventType) && !_.isArray(eventType)) { - throw new Error('"eventType" should be a string or an array'); - } - - url.query.event_type = eventType; - } - return new EventSource(nodeUrl.format(url), opts); - } - }; -}; +module.exports = MarathonEventSource diff --git a/lib/groups.js b/lib/groups.js index a9de320..f07db60 100644 --- a/lib/groups.js +++ b/lib/groups.js @@ -1,21 +1,35 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/groups - getList: function getList() { - return makeRequest('GET', '/groups')(); - }, - create: function create(body) { - return makeRequest('POST', '/groups')(null, body); - }, - // /v2/groups/{group_id} - getOne: function getOne(groupId) { - return makeRequest('GET', '/groups/' + groupId)(); - }, - update: function update(groupId, body, force) { - return makeRequest('PUT', '/groups/' + groupId)({force: force}, body); - }, - destroy: function destroy(groupId, force) { - return makeRequest('DELETE', '/groups/' + groupId)({force: force}); - } - }; -}; +'use strict' + +class MarathonApiGroupEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/groups` + this.client = ctx.http.create() + this.client.defaults.baseURL = this.baseURL.toString() + } + + async getList () { + return this.client.get() + } + + create (data) { + return this.client.post('', { json: data }) + } + + getOne (groupId) { + return this.client.get(groupId) + } + + update (groupId, data, force) { + const params = new URLSearchParams([['force', force]]) + return this.client.put(groupId, { json: data }, { params }) + } + + async destroy (groupId, force) { + const params = new URLSearchParams([['force', force]]) + return this.client.delete(groupId, undefined, { params }) + } +} + +module.exports = MarathonApiGroupEndpoints diff --git a/lib/info.js b/lib/info.js index 5ae80d5..58cbb6e 100644 --- a/lib/info.js +++ b/lib/info.js @@ -1,8 +1,17 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/info - get: function get() { - return makeRequest('GET', '/info')(); - } - }; -}; +'use strict' + +class MarathonApiInfoEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/info` + this.client = ctx.http.create() + this.client.defaults.baseURL = this.baseURL.toString() + } + + async get () { + return this.client.get() + } +} + +module.exports = MarathonApiInfoEndpoints diff --git a/lib/leader.js b/lib/leader.js index 9df2fb6..cd17806 100644 --- a/lib/leader.js +++ b/lib/leader.js @@ -1,11 +1,21 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/leader - get: function get() { - return makeRequest('GET', '/leader')(); - }, - abdicate: function abdicate() { - return makeRequest('DELETE', '/leader')(); - } - }; -}; +'use strict' + +class MarathonApiLeaderEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/leader` + this.client = ctx.http.create() + this.client.defaults.baseURL = this.baseURL.toString() + } + + async get () { + return this.client.get() + } + + async abdicate () { + return this.client.delete() + } +} + +module.exports = MarathonApiLeaderEndpoints diff --git a/lib/marathon.js b/lib/marathon.js index e2ff921..a887280 100644 --- a/lib/marathon.js +++ b/lib/marathon.js @@ -1,111 +1,77 @@ -'use strict'; - -var rp = require('request-promise'); -var request = require('request'); -var Promise = require('bluebird'); -var _ = require('lodash'); -var nodeUrl = require('url'); -var nodePath = require('path'); - -var API_VERSION = 'v2'; -var PATHS_WITHOUT_PREFIX = ['/ping', '/metrics']; - -var apps = require('./apps'); -var deployments = require('./deployments'); -var groups = require('./groups'); -var tasks = require('./tasks'); -var artifacts = require('./artifacts'); -var events = require('./events'); -var subscriptions = require('./subscriptions'); -var info = require('./info'); -var leader = require('./leader'); -var queue = require('./queue'); -var misc = require('./misc'); - +'use strict' + +const { omit } = require('./nodash') +const axios = require('axios') + +const MARATHON_API_VERSION = 'v2' +// http api endpoints +const MarathonApiAppEndpoints = require('./apps') +const MarathonApiDeploymentEndpoints = require('./deployments') +const MarathonApiGroupEndpoints = require('./groups') +const MarathonApiTaskEndpoints = require('./tasks') +const MarathonApiInfoEndpoints = require('./info') +const MarathonApiQueueEndpoints = require('./queue') +const MarathonApiMiscEndpoints = require('./misc') +const MarathonApiLeaderEndpoints = require('./leader') + +// EventSource +const MarathonEventSource = require('./events') + +// not yet implemented so no need to require here +// var artifacts = require('./artifacts') + +const _timeToken = Symbol('timetoken') +const contentType = 'application/json' + +axios.defaults.headers.post['Content-Type'] = contentType +axios.defaults.headers.put['Content-Type'] = contentType +axios.defaults.headers.delete['Content-Type'] = contentType /** * https://mesosphere.github.io/marathon/docs/rest-api.html */ -function Marathon(baseUrl, opts) { - opts = opts || {}; - - var baseOptions = { - json: true - }; - - _.assign(baseOptions, _.omit(opts, 'logTime')); - - function getRequestUrl(path) { - if (PATHS_WITHOUT_PREFIX.indexOf(path) < 0) { - path = nodePath.join('/', API_VERSION, path); - } - - var result = nodeUrl.parse(baseUrl); - result.pathname = nodePath.join(result.pathname, path); - return result; +class MarathonApi { + get basePath () { return `/${MARATHON_API_VERSION}` } + + constructor (baseURL, opts = {}) { + this.baseURL = new URL(`${baseURL}`) // if it's already a url object create a new copy + this.logTime = opts.logTime + this.baseURL.pathname = this.basePath + const baseOpts = omit(opts, 'logTime') // setup options without the logTime + + this.http = axios.create({ + ...baseOpts, + baseURL: this.baseURL.toString() // axios needs a string + }) + if (this.logTime) { + this.http.interceptors.response.use(this.timeTracker) + this.http.interceptors.request.use(this.timeTracker) } - function makeRequest(method, path, addOptions, requestStream) { - return function closure(query, body) { - var requestOptions = _.cloneDeep(baseOptions); - - requestOptions.method = method; - requestOptions.qs = query; - requestOptions.url = nodeUrl.format(getRequestUrl(path)); - - if (addOptions) { - requestOptions = _.extend(requestOptions, addOptions); - } - - requestOptions.body = body; - - var consoleTimeToken = 'Request to Marathon ' + path; - - if (opts.logTime) { - console.time(consoleTimeToken); - } - - function logTime() { - if (opts.logTime) { - console.timeEnd(consoleTimeToken); - } - } - - if (requestStream) { - return new Promise(function createPromise(resolve, reject) { - request[method.toLowerCase()](requestOptions) - .on('error', function onError(err) { - return reject(err); - }) - .on('response', function onResponse(readableStream) { - readableStream.on('end', function onEnd() { - logTime(); - }); - return resolve(readableStream); - }); - }); - } - - return rp(requestOptions).finally(logTime); - }; + // initialize the api endpoints + this.app = new MarathonApiAppEndpoints(this) + this.deployments = new MarathonApiDeploymentEndpoints(this) + this.events = new MarathonEventSource(this) + this.groups = new MarathonApiGroupEndpoints(this) + this.tasks = new MarathonApiTaskEndpoints(this) + this.info = new MarathonApiInfoEndpoints(this) + this.queue = new MarathonApiQueueEndpoints(this) + this.misc = new MarathonApiMiscEndpoints(this) + this.leader = new MarathonApiLeaderEndpoints(this) + } + + set timeToken (newtoken) { this[_timeToken] = newtoken } + get timeToken () { return this[_timeToken] } + + async timeTracker (c) { + if (this.logTime && c?.url) { + this.timeToken = `Marathon Request: ${c.url}` + // assume it's on the request side + console.time(this.timeToken) + } else if (this.logTime && c?.status) { + console.timeEnd(this.timeToken) } - - var client = { - app: apps(makeRequest), - deployments: deployments(makeRequest), - groups: groups(makeRequest), - tasks: tasks(makeRequest), - artifacts: artifacts(makeRequest), - events: events(makeRequest, getRequestUrl('/events')), - subscriptions: subscriptions(makeRequest), - info: info(makeRequest), - leader: leader(makeRequest), - queue: queue(makeRequest), - misc: misc(makeRequest) - }; - - client.apps = client.app; - - return client; + return c + } } -module.exports = Marathon; +module.exports = MarathonApi diff --git a/lib/misc.js b/lib/misc.js index 944cd57..d0ed4b5 100644 --- a/lib/misc.js +++ b/lib/misc.js @@ -1,8 +1,23 @@ -module.exports = function createMethods(makeRequest) { - return { - // /ping - ping: function ping(addOptions) { - return makeRequest('GET', '/ping', addOptions)(); - } - }; -}; +'use strict' + +class MarathonApiMiscEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.client = ctx.http + this.baseURL = new URL(this.client.defaults.baseURL) + this.baseURL.pathname = '/' // these endpoints don't get the version prefix + this.client.defaults.baseURL = this.baseURL.toString() + } + + async ping () { + this.timeToken = '/ping' + return this.client.get('ping') + } + + async metrics () { + this.timeToken = '/metrics' + return this.client.get('metrics') + } +} + +module.exports = MarathonApiMiscEndpoints diff --git a/lib/nodash.js b/lib/nodash.js new file mode 100644 index 0000000..3cdd21e --- /dev/null +++ b/lib/nodash.js @@ -0,0 +1,99 @@ +'use strict' + +const omit = function (obj, ...keys) { + const keysToRemove = new Set(keys.flat()) // flatten the props, and convert to a Set + + return Object.fromEntries( // convert the entries back to object + Object.entries(obj) // convert the object to entries + .filter(([k]) => !keysToRemove.has(k)) // remove entries with keys that exist in the Set + ) +} + +// maps an object similarly to lodash.map function +const mapObject = function (obj, fun) { + if (!fun) fun = (...opts) => opts + return Object.entries(obj).map(v => fun(...v)) +} + +// determine whether or not an object is empty +const emptyObj = obj => Object.keys(obj).length === 0 + +const pick = (obj, ...keys) => { + const result = {} + for (const key of keys) { + result[key] = obj[key] + } + return result +} + +const difference = (a, b) => a.filter(x => b.includes(x)) + +const union = (...arrays) => [...new Set([...arrays.flat()])] + +const unionBy = (...input) => { + const output = [] + const x = input.pop() + for (const a of input) { + for (const i of a) { + if (i?.[x]) { + const f = output.find(y => y?.[x] === i?.[x]) + if (!f) output.push(i) + } + } + } + return output +} + +const debounce = function (fn, delay) { + let timer = null + return function () { + clearTimeout(timer) + timer = setTimeout(() => fn.apply(this, arguments), delay) + } +} + +const throttle = function (fn, threshold, scope) { + threshold || (threshold = 250) + scope || (scope = this) + let last + let deferTimer + let result + return async function Throttled () { + const now = +new Date() + if (last && now < last + threshold) { + clearTimeout(deferTimer) + deferTimer = setTimeout(async () => { + last = now + result = await fn.apply(scope, arguments) + }, threshold) + } else { + last = now + result = await fn.apply(scope, arguments) + } + return result + } +} + +const partition = function (col, fn) { + return [...col].reduce((acc, value, key) => { + acc[fn(value, key) ? 0 : 1].push(value) + return acc + }, [[], []]) +} + +// returns a function with some of it's arguments filled out +const partial = (fn, ...params) => (...more) => fn(...params, ...more) + +module.exports = { + emptyObj, + debounce, + difference, + mapObject, + omit, + partial, + partition, + pick, + throttle, + union, + unionBy +} diff --git a/lib/queue.js b/lib/queue.js index df33001..d6c6316 100644 --- a/lib/queue.js +++ b/lib/queue.js @@ -1,12 +1,21 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/queue - get: function get() { - return makeRequest('GET', '/queue')(); - }, - // /v2/queue/{app_id}/delay - resetDelay: function resetDelay(appId) { - return makeRequest('DELETE', '/queue/' + appId + '/delay')(); - } - }; -}; +'use strict' + +class MarathonApiQueueEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/queue` + this.client = ctx.http.create() + this.client.defaults.baseURL = this.baseURL.toString() + } + + async get () { + return this.client.get() + } + + async resetDelay (appId) { + return this.client.delete(`${appId}/delay`) + } +} + +module.exports = MarathonApiQueueEndpoints diff --git a/lib/tasks.js b/lib/tasks.js index 19a8461..f7c1d09 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -1,12 +1,22 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/tasks - getList: function getList() { - return makeRequest('GET', '/tasks')(); - }, - // /v2/tasks/delete - kill: function kill(body, scale, wipe) { - return makeRequest('POST', '/tasks/delete')({scale: scale, wipe: wipe}, body); - } - }; -}; +'use strict' + +class MarathonApiTaskEndpoints { + constructor (ctx) { // accepts a parent context + this.parent = ctx + this.baseURL = ctx.baseURL + this.baseURL.pathname = `${ctx.basePath}/tasks` + this.client = ctx.http.create() + this.client.defaults.baseURL = this.baseURL.toString() + } + + async getList () { + return this.client.get() + } + + async kill (data, scale, wipe) { + const params = new URLSearchParams(Object.entries({ scale, wipe })) + return this.client.post('delete', { json: data }, { params }) + } +} + +module.exports = MarathonApiTaskEndpoints diff --git a/package.json b/package.json index 361fdab..f53a66a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marathon-node", - "version": "1.1.0", + "version": "2.0.0", "description": "Node.js client library for Mesos Marathon's REST API", "main": "index.js", "scripts": { @@ -11,20 +11,17 @@ }, "repository": { "type": "git", - "url": "https://github.com/elasticio/marathon-node.git" + "url": "https://github.com/AppliedTrust/marathon-node.git" }, - "author": "elastic.io", + "author": "AppliedTrust", "license": "ISC", "bugs": { - "url": "https://github.com/elasticio/marathon-node/issues" + "url": "https://github.com/AppliedTrust/marathon-node/issues" }, - "homepage": "https://github.com/elasticio/marathon-node", + "homepage": "https://github.com/AppliedTrust/marathon-node", "dependencies": { - "bluebird": "^3.4.7", - "eventsource": "^1.0.4", - "lodash": "^4.0.0", - "request": "2.75.x", - "request-promise": "^3.0.0" + "axios": "^1.3.3", + "eventsource": "^2.0.2" }, "devDependencies": { "chai": "^3.5.0", diff --git a/postpublish.js b/postpublish.js index c966663..4a1076b 100755 --- a/postpublish.js +++ b/postpublish.js @@ -1,24 +1,24 @@ #! /usr/bin/env node -'use strict'; +'use strict' -var execSync = require('child_process').execSync; -var version = require('./package.json').version; +var { execSync } = require('child_process') +var { version } = require('./package.json') if (!version) { - console.error('Can not determine current version'); - process.exit(0); + console.error('Can not determine current version') + process.exit(0) } -var tag = 'v' + version; +var tag = 'v' + version try { - // if grep not found anything, it's exit code isn't zero, so execSync raises an Error - execSync('git tag | grep "' + tag + '"'); - // it seems tag is found, do nothing - process.exit(0); + // if grep not found anything, it's exit code isn't zero, so execSync raises an Error + execSync('git tag | grep "' + tag + '"') + // it seems tag is found, do nothing + process.exit(0) } catch (e) { - // grep found nothing, so le'ts create new tag - console.info('creating a new tag: ', tag); - execSync('git tag ' + tag); - console.info('pushing tag to origin: ', tag); - execSync('git push origin ' + tag); + // grep found nothing, so le'ts create new tag + console.info('creating a new tag: ', tag) + execSync('git tag ' + tag) + console.info('pushing tag to origin: ', tag) + execSync('git push origin ' + tag) } From b09dbc65afea08f946cf85a30a7b6a3b49efc823 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Fri, 24 Feb 2023 09:35:53 -0500 Subject: [PATCH 05/13] separate lib from api files and move main MarathonApi to index --- {lib => api}/apps.js | 33 ++++++++++----- api/artifacts.js | 6 +++ {lib => api}/deployments.js | 6 ++- {lib => api}/events.js | 0 {lib => api}/groups.js | 21 ++++++---- {lib => api}/info.js | 5 ++- {lib => api}/leader.js | 6 ++- {lib => api}/misc.js | 8 ++-- {lib => api}/queue.js | 6 ++- {lib => api}/tasks.js | 6 ++- index.js | 83 ++++++++++++++++++++++++++++++++++++- lib/artifacts.js | 6 --- lib/marathon.js | 77 ---------------------------------- lib/subscriptions.js | 12 ------ package.json | 2 +- 15 files changed, 148 insertions(+), 129 deletions(-) rename {lib => api}/apps.js (66%) create mode 100644 api/artifacts.js rename {lib => api}/deployments.js (76%) rename {lib => api}/events.js (100%) rename {lib => api}/groups.js (51%) rename {lib => api}/info.js (76%) rename {lib => api}/leader.js (76%) rename {lib => api}/misc.js (72%) rename {lib => api}/queue.js (74%) rename {lib => api}/tasks.js (74%) delete mode 100644 lib/artifacts.js delete mode 100644 lib/marathon.js delete mode 100644 lib/subscriptions.js diff --git a/lib/apps.js b/api/apps.js similarity index 66% rename from lib/apps.js rename to api/apps.js index 1139b4e..3dc6040 100644 --- a/lib/apps.js +++ b/api/apps.js @@ -13,62 +13,73 @@ class MarathonApiAppEndpoints { async getList (query = {}) { const params = new URLSearchParams(Object.entries(query)) console.log({ clientDefaults: this.client.defaults }) - return this.client.get('', { params }) + const { data } = await this.client.get('', { params }) + return data } async create (data) { - return this.client.post('', data) + const { data: result } = await this.client.post('', data) + return result } async getOne (appId, query = {}) { const params = new URLSearchParams(Object.entries(query)) const path = encodeURIComponent(appId) - return this.client.get(path, { params }) + const { data } = await this.client.get(path, { params }) + return data } async update (appId, data, force) { const params = new URLSearchParams([['force', !!force]]) const path = encodeURIComponent(appId) - return this.client.put(path, data, { params }) + const { data: result } = await this.client.put(path, data, { params }) + return result } async destroy (appId, force) { const params = new URLSearchParams([['force', !!force]]) const path = encodeURIComponent(appId) - return this.client.delete(path, undefined, { params }) + const { data } = await this.client.delete(path, undefined, { params }) + return data } async restart (appId, force) { const params = new URLSearchParams([['force', !!force]]) const path = `${encodeURIComponent(appId)}/restart` - return this.client.post(path, undefined, { params }) + const { data } = await this.client.post(path, undefined, { params }) + return data } async getTasks (appId) { const path = `${encodeURIComponent(appId)}/tasks` - return this.client.get(`${path}/tasks`) + const { data } = await this.client.get(`${path}/tasks`) + return data } async killTasks (appId, query = {}) { const params = new URLSearchParams(Object.entries(query)) const path = `${encodeURIComponent(appId)}/tasks` - return this.client.delete(path, undefined, { params }) + const { data } = await this.client.delete(path, undefined, { params }) + return data } async killTask (appId, taskId, scale) { const params = new URLSearchParams([['scale', !!scale]]) const path = `${encodeURIComponent(appId)}/tasks/${taskId}` - return this.client.delete(path, undefined, { params }) + const { data } = await this.client.delete(path, undefined, { params }) + return data } async getVersions (appId) { const path = `${encodeURIComponent(appId)}/versions` - return this.client.get(path) + const { data } = await this.client.get(path) + return data } async getVersion (appId, versionId) { const path = `${encodeURIComponent(appId)}/versions/${versionId}` - return this.client.get(path) + const { data } = await this.client.get(path) + return data } } diff --git a/api/artifacts.js b/api/artifacts.js new file mode 100644 index 0000000..07c254d --- /dev/null +++ b/api/artifacts.js @@ -0,0 +1,6 @@ +module.exports = function createMethods (makeRequest) { + return { + // /v2/artifacts todo + // /v2/artifacts/{path} todo + } +} diff --git a/lib/deployments.js b/api/deployments.js similarity index 76% rename from lib/deployments.js rename to api/deployments.js index a6de192..b017eac 100644 --- a/lib/deployments.js +++ b/api/deployments.js @@ -10,12 +10,14 @@ class MarathonApiDeploymentEndpoints { } async getList () { - return this.client.get() + const { data } = await this.client.get() + return data } async destroy (deploymentId, force) { const params = new URLSearchParams([['force', force]]) - return this.client.delete(deploymentId, { params }) + const { data } = await this.client.delete(deploymentId, { params }) + return data } } diff --git a/lib/events.js b/api/events.js similarity index 100% rename from lib/events.js rename to api/events.js diff --git a/lib/groups.js b/api/groups.js similarity index 51% rename from lib/groups.js rename to api/groups.js index f07db60..1ebb76b 100644 --- a/lib/groups.js +++ b/api/groups.js @@ -10,25 +10,30 @@ class MarathonApiGroupEndpoints { } async getList () { - return this.client.get() + const { data } = await this.client.get() + return data } - create (data) { - return this.client.post('', { json: data }) + async create (data) { + const { data: result } = await this.client.post('', { json: data }) + return result } - getOne (groupId) { - return this.client.get(groupId) + async getOne (groupId) { + const { data } = await this.client.get(groupId) + return data } - update (groupId, data, force) { + async update (groupId, data, force) { const params = new URLSearchParams([['force', force]]) - return this.client.put(groupId, { json: data }, { params }) + const { data: result } = await this.client.put(groupId, { json: data }, { params }) + return result } async destroy (groupId, force) { const params = new URLSearchParams([['force', force]]) - return this.client.delete(groupId, undefined, { params }) + const { data } = await this.client.delete(groupId, undefined, { params }) + return data } } diff --git a/lib/info.js b/api/info.js similarity index 76% rename from lib/info.js rename to api/info.js index 58cbb6e..8febcff 100644 --- a/lib/info.js +++ b/api/info.js @@ -9,8 +9,9 @@ class MarathonApiInfoEndpoints { this.client.defaults.baseURL = this.baseURL.toString() } - async get () { - return this.client.get() + async get (timeout = 1000) { + const { data } = await this.client.get('', { timeout }) + return data } } diff --git a/lib/leader.js b/api/leader.js similarity index 76% rename from lib/leader.js rename to api/leader.js index cd17806..782749d 100644 --- a/lib/leader.js +++ b/api/leader.js @@ -10,11 +10,13 @@ class MarathonApiLeaderEndpoints { } async get () { - return this.client.get() + const { data } = await this.client.get() + return data } async abdicate () { - return this.client.delete() + const { data } = await this.client.delete() + return data } } diff --git a/lib/misc.js b/api/misc.js similarity index 72% rename from lib/misc.js rename to api/misc.js index d0ed4b5..e1927c7 100644 --- a/lib/misc.js +++ b/api/misc.js @@ -9,14 +9,16 @@ class MarathonApiMiscEndpoints { this.client.defaults.baseURL = this.baseURL.toString() } - async ping () { + async ping (timeout = 300) { this.timeToken = '/ping' - return this.client.get('ping') + const { data } = await this.client.get('ping', { timeout }) + return data } async metrics () { this.timeToken = '/metrics' - return this.client.get('metrics') + const { data } = await this.client.get('metrics') + return data } } diff --git a/lib/queue.js b/api/queue.js similarity index 74% rename from lib/queue.js rename to api/queue.js index d6c6316..c3653bb 100644 --- a/lib/queue.js +++ b/api/queue.js @@ -10,11 +10,13 @@ class MarathonApiQueueEndpoints { } async get () { - return this.client.get() + const { data } = await this.client.get() + return data } async resetDelay (appId) { - return this.client.delete(`${appId}/delay`) + const { data } = await this.client.delete(`${appId}/delay`) + return data } } diff --git a/lib/tasks.js b/api/tasks.js similarity index 74% rename from lib/tasks.js rename to api/tasks.js index f7c1d09..368804b 100644 --- a/lib/tasks.js +++ b/api/tasks.js @@ -10,12 +10,14 @@ class MarathonApiTaskEndpoints { } async getList () { - return this.client.get() + const { data } = await this.client.get() + return data } async kill (data, scale, wipe) { const params = new URLSearchParams(Object.entries({ scale, wipe })) - return this.client.post('delete', { json: data }, { params }) + const { data: result } = await this.client.post('delete', { json: data }, { params }) + return result } } diff --git a/index.js b/index.js index 734afe8..70d3fba 100644 --- a/index.js +++ b/index.js @@ -1 +1,82 @@ -module.exports = require('./lib/marathon'); \ No newline at end of file +'use strict' + +const axios = require('axios') + +const MARATHON_API_VERSION = 'v2' +// http api endpoints +const MarathonApiAppEndpoints = require('./api/apps') +const MarathonApiDeploymentEndpoints = require('./api/deployments') +const MarathonApiGroupEndpoints = require('./api/groups') +const MarathonApiTaskEndpoints = require('./api/tasks') +const MarathonApiInfoEndpoints = require('./api/info') +const MarathonApiQueueEndpoints = require('./api/queue') +const MarathonApiMiscEndpoints = require('./api/misc') +const MarathonApiLeaderEndpoints = require('./api/leader') + +// EventSource +const MarathonEventSource = require('./events') + +// not yet implemented so no need to require here +// var artifacts = require('./artifacts') + +// helper functions +const { omit } = require('./lib/nodash') + +const _timeToken = Symbol('timetoken') +const contentType = 'application/json' + +// configure axios defaults +axios.defaults.headers.common.Accept = contentType +axios.defaults.headers.post['Content-Type'] = contentType +axios.defaults.headers.put['Content-Type'] = contentType +axios.defaults.headers.delete['Content-Type'] = contentType +/** + * https://mesosphere.github.io/marathon/docs/rest-api.html + */ +class MarathonApi { + get basePath () { return `/${MARATHON_API_VERSION}` } + + constructor (baseURL, opts = {}) { + this.baseURL = new URL(`${baseURL}`) // if it's already a url object create a new copy + this.logTime = opts.logTime + this.baseURL.pathname = this.basePath + const baseOpts = omit(opts, 'logTime') // setup options without the logTime + + this.http = axios.create({ + ...baseOpts, + baseURL: this.baseURL.toString() // axios needs a string + }) + if (this.logTime) { + this.http.interceptors.response.use(this.timeTracker) + this.http.interceptors.request.use(this.timeTracker) + } + + // initialize the api endpoints + this.app = new MarathonApiAppEndpoints(this) + this.apps = this.app // alias for app + this.deployments = new MarathonApiDeploymentEndpoints(this) + this.events = new MarathonEventSource(this) + this.groups = new MarathonApiGroupEndpoints(this) + this.tasks = new MarathonApiTaskEndpoints(this) + this.info = new MarathonApiInfoEndpoints(this) + this.queue = new MarathonApiQueueEndpoints(this) + this.misc = new MarathonApiMiscEndpoints(this) + this.leader = new MarathonApiLeaderEndpoints(this) + } + + set timeToken (newtoken) { this[_timeToken] = newtoken } + get timeToken () { return this[_timeToken] } + + async timeTracker (c) { + if (this.logTime && c?.url) { + this.timeToken = `Marathon Request: ${c.url}` + // assume it's on the request side + console.time(this.timeToken) + } else if (this.logTime && c?.status) { + console.timeEnd(this.timeToken) + } + return c + } +} + +module.exports = MarathonApi diff --git a/lib/artifacts.js b/lib/artifacts.js deleted file mode 100644 index 447aeb2..0000000 --- a/lib/artifacts.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/artifacts todo - // /v2/artifacts/{path} todo - }; -}; diff --git a/lib/marathon.js b/lib/marathon.js deleted file mode 100644 index a887280..0000000 --- a/lib/marathon.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict' - -const { omit } = require('./nodash') -const axios = require('axios') - -const MARATHON_API_VERSION = 'v2' -// http api endpoints -const MarathonApiAppEndpoints = require('./apps') -const MarathonApiDeploymentEndpoints = require('./deployments') -const MarathonApiGroupEndpoints = require('./groups') -const MarathonApiTaskEndpoints = require('./tasks') -const MarathonApiInfoEndpoints = require('./info') -const MarathonApiQueueEndpoints = require('./queue') -const MarathonApiMiscEndpoints = require('./misc') -const MarathonApiLeaderEndpoints = require('./leader') - -// EventSource -const MarathonEventSource = require('./events') - -// not yet implemented so no need to require here -// var artifacts = require('./artifacts') - -const _timeToken = Symbol('timetoken') -const contentType = 'application/json' - -axios.defaults.headers.post['Content-Type'] = contentType -axios.defaults.headers.put['Content-Type'] = contentType -axios.defaults.headers.delete['Content-Type'] = contentType -/** - * https://mesosphere.github.io/marathon/docs/rest-api.html - */ -class MarathonApi { - get basePath () { return `/${MARATHON_API_VERSION}` } - - constructor (baseURL, opts = {}) { - this.baseURL = new URL(`${baseURL}`) // if it's already a url object create a new copy - this.logTime = opts.logTime - this.baseURL.pathname = this.basePath - const baseOpts = omit(opts, 'logTime') // setup options without the logTime - - this.http = axios.create({ - ...baseOpts, - baseURL: this.baseURL.toString() // axios needs a string - }) - if (this.logTime) { - this.http.interceptors.response.use(this.timeTracker) - this.http.interceptors.request.use(this.timeTracker) - } - - // initialize the api endpoints - this.app = new MarathonApiAppEndpoints(this) - this.deployments = new MarathonApiDeploymentEndpoints(this) - this.events = new MarathonEventSource(this) - this.groups = new MarathonApiGroupEndpoints(this) - this.tasks = new MarathonApiTaskEndpoints(this) - this.info = new MarathonApiInfoEndpoints(this) - this.queue = new MarathonApiQueueEndpoints(this) - this.misc = new MarathonApiMiscEndpoints(this) - this.leader = new MarathonApiLeaderEndpoints(this) - } - - set timeToken (newtoken) { this[_timeToken] = newtoken } - get timeToken () { return this[_timeToken] } - - async timeTracker (c) { - if (this.logTime && c?.url) { - this.timeToken = `Marathon Request: ${c.url}` - // assume it's on the request side - console.time(this.timeToken) - } else if (this.logTime && c?.status) { - console.timeEnd(this.timeToken) - } - return c - } -} - -module.exports = MarathonApi diff --git a/lib/subscriptions.js b/lib/subscriptions.js deleted file mode 100644 index 79da4e9..0000000 --- a/lib/subscriptions.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = function createMethods(makeRequest) { - return { - // /v2/eventSubscriptions - getList: makeRequest('GET', '/eventSubscriptions'), - create: function create(callbackUrl) { - return makeRequest('POST', '/eventSubscriptions')({qs: "callbackUrl=" + callbackUrl}); - }, - delete: function deleteSubscription(callbackUrl) { - return makeRequest('DELETE', '/eventSubscriptions')({qs: "callbackUrl=" + callbackUrl}); - } - }; -}; diff --git a/package.json b/package.json index f53a66a..64ad3fb 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "homepage": "https://github.com/AppliedTrust/marathon-node", "dependencies": { - "axios": "^1.3.3", + "axios": "^1.3.4", "eventsource": "^2.0.2" }, "devDependencies": { From 148832c23669b642b049a6a456b984551163e104 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Fri, 24 Feb 2023 09:41:22 -0500 Subject: [PATCH 06/13] make sure to include events api in new location --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 70d3fba..a95ebd5 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ const MarathonApiMiscEndpoints = require('./api/misc') const MarathonApiLeaderEndpoints = require('./api/leader') // EventSource -const MarathonEventSource = require('./events') +const MarathonEventSource = require('./api/events') // not yet implemented so no need to require here // var artifacts = require('./artifacts') From d62fcef1ce78c572578a98b07081ef6bb06aad4b Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Fri, 24 Feb 2023 12:06:55 -0500 Subject: [PATCH 07/13] fix time logging --- api/apps.js | 4 +--- api/deployments.js | 2 +- api/events.js | 2 +- api/groups.js | 2 +- api/info.js | 2 +- api/leader.js | 2 +- api/misc.js | 4 ++-- api/queue.js | 2 +- api/tasks.js | 2 +- index.js | 43 ++++++++++++++++++++++++------------------- 10 files changed, 34 insertions(+), 31 deletions(-) diff --git a/api/apps.js b/api/apps.js index 3dc6040..1fed6bf 100644 --- a/api/apps.js +++ b/api/apps.js @@ -5,14 +5,12 @@ class MarathonApiAppEndpoints { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/apps` - this.client = ctx.http.create() - this.client.interceptors = ctx.http.interceptors + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } async getList (query = {}) { const params = new URLSearchParams(Object.entries(query)) - console.log({ clientDefaults: this.client.defaults }) const { data } = await this.client.get('', { params }) return data } diff --git a/api/deployments.js b/api/deployments.js index b017eac..33ef6cc 100644 --- a/api/deployments.js +++ b/api/deployments.js @@ -5,7 +5,7 @@ class MarathonApiDeploymentEndpoints { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/deployments` - this.client = ctx.http.create() + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/api/events.js b/api/events.js index 716fee9..bde620d 100644 --- a/api/events.js +++ b/api/events.js @@ -7,7 +7,7 @@ class MarathonEventSource { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/events` - this.client = ctx.http.create() + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/api/groups.js b/api/groups.js index 1ebb76b..7fb654f 100644 --- a/api/groups.js +++ b/api/groups.js @@ -5,7 +5,7 @@ class MarathonApiGroupEndpoints { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/groups` - this.client = ctx.http.create() + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/api/info.js b/api/info.js index 8febcff..a55c51a 100644 --- a/api/info.js +++ b/api/info.js @@ -5,7 +5,7 @@ class MarathonApiInfoEndpoints { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/info` - this.client = ctx.http.create() + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/api/leader.js b/api/leader.js index 782749d..951db6e 100644 --- a/api/leader.js +++ b/api/leader.js @@ -5,7 +5,7 @@ class MarathonApiLeaderEndpoints { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/leader` - this.client = ctx.http.create() + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/api/misc.js b/api/misc.js index e1927c7..9de791f 100644 --- a/api/misc.js +++ b/api/misc.js @@ -3,9 +3,9 @@ class MarathonApiMiscEndpoints { constructor (ctx) { // accepts a parent context this.parent = ctx - this.client = ctx.http - this.baseURL = new URL(this.client.defaults.baseURL) + this.baseURL = ctx.baseURL this.baseURL.pathname = '/' // these endpoints don't get the version prefix + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/api/queue.js b/api/queue.js index c3653bb..1e2a283 100644 --- a/api/queue.js +++ b/api/queue.js @@ -5,7 +5,7 @@ class MarathonApiQueueEndpoints { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/queue` - this.client = ctx.http.create() + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/api/tasks.js b/api/tasks.js index 368804b..956f6e6 100644 --- a/api/tasks.js +++ b/api/tasks.js @@ -5,7 +5,7 @@ class MarathonApiTaskEndpoints { this.parent = ctx this.baseURL = ctx.baseURL this.baseURL.pathname = `${ctx.basePath}/tasks` - this.client = ctx.http.create() + this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } diff --git a/index.js b/index.js index a95ebd5..4350d03 100644 --- a/index.js +++ b/index.js @@ -36,20 +36,24 @@ axios.defaults.headers.delete['Content-Type'] = contentType class MarathonApi { get basePath () { return `/${MARATHON_API_VERSION}` } - constructor (baseURL, opts = {}) { - this.baseURL = new URL(`${baseURL}`) // if it's already a url object create a new copy - this.logTime = opts.logTime - this.baseURL.pathname = this.basePath - const baseOpts = omit(opts, 'logTime') // setup options without the logTime - - this.http = axios.create({ - ...baseOpts, - baseURL: this.baseURL.toString() // axios needs a string + get http () { // returns an http client + const client = axios.create({ + ...this.defaults, + baseURL: this.baseURL.toString() }) if (this.logTime) { - this.http.interceptors.response.use(this.timeTracker) - this.http.interceptors.request.use(this.timeTracker) + client.interceptors.request.use(async config => this.timeTracker(config)) + client.interceptors.response.use(async res => this.timeTracker(res)) } + return client + } + + constructor (baseURL, opts = {}) { + this.logTime = opts.logTime + this.defaults = omit(opts, 'logTime') // setup options without the logTime + + this.baseURL = new URL(`${baseURL}`) // if it's already a url object create a new copy + this.baseURL.pathname = this.basePath // initialize the api endpoints this.app = new MarathonApiAppEndpoints(this) @@ -62,18 +66,19 @@ class MarathonApi { this.queue = new MarathonApiQueueEndpoints(this) this.misc = new MarathonApiMiscEndpoints(this) this.leader = new MarathonApiLeaderEndpoints(this) + this.timers = {} // key/object pairing of timer tokens } - set timeToken (newtoken) { this[_timeToken] = newtoken } - get timeToken () { return this[_timeToken] } - async timeTracker (c) { - if (this.logTime && c?.url) { - this.timeToken = `Marathon Request: ${c.url}` - // assume it's on the request side - console.time(this.timeToken) + if (this.logTime && !c.status) { + const { pathname } = new URL(c.baseURL) + if (!(pathname in this.timers)) { + this.timers[pathname] = `Marathon Request Timer: ${pathname}` + } + console.time(this.timers[pathname]) } else if (this.logTime && c?.status) { - console.timeEnd(this.timeToken) + const { request: { path } } = c + console.timeEnd(this.timers[path]) } return c } From 4fa33ece018a73b435647ca22af936138637c72c Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Tue, 28 Feb 2023 07:34:03 -0500 Subject: [PATCH 08/13] remove unused symbol --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index 4350d03..cc8e377 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,6 @@ const MarathonEventSource = require('./api/events') // helper functions const { omit } = require('./lib/nodash') -const _timeToken = Symbol('timetoken') const contentType = 'application/json' // configure axios defaults From 956958506b943b35c3d2ae3c61453ed65adee5f6 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Tue, 28 Feb 2023 10:20:44 -0500 Subject: [PATCH 09/13] update nodash to match current state --- lib/nodash.js | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/nodash.js b/lib/nodash.js index 3cdd21e..8b7bcbd 100644 --- a/lib/nodash.js +++ b/lib/nodash.js @@ -1,4 +1,8 @@ -'use strict' +// disabled so we can use `with` in the template function +// for now until a better solution presents itself +// 'use strict' + +// const AsyncFunction = (async () => {}).constructor const omit = function (obj, ...keys) { const keysToRemove = new Set(keys.flat()) // flatten the props, and convert to a Set @@ -15,9 +19,28 @@ const mapObject = function (obj, fun) { return Object.entries(obj).map(v => fun(...v)) } +const isObject = obj => (obj !== null && typeof obj === 'object') + // determine whether or not an object is empty const emptyObj = obj => Object.keys(obj).length === 0 +const isEqual = (o1, o2) => { // deep object comparison + if (isObject(o1) && isObject(o2)) { + const k1 = Object.keys(o1) + const k2 = Object.keys(o2) + const keq = difference(k1, k2) + if (keq.length) return false // key difference, easy + for (const k of k1) { + const v1 = k1[k] + const v2 = k2[k] + const deep = (isObject(v1) && isObject(v2)) + if ((deep && !isEqual(v1, v2)) || (!deep && v1 !== v2)) return false + } + return true + } + return (o1 === o2) // basic comparison +} + const pick = (obj, ...keys) => { const result = {} for (const key of keys) { @@ -26,7 +49,7 @@ const pick = (obj, ...keys) => { return result } -const difference = (a, b) => a.filter(x => b.includes(x)) +const difference = (a, b) => a.filter(x => !b.includes(x)) const union = (...arrays) => [...new Set([...arrays.flat()])] @@ -84,7 +107,16 @@ const partition = function (col, fn) { // returns a function with some of it's arguments filled out const partial = (fn, ...params) => (...more) => fn(...params, ...more) +// probably not suitable for anything with unsanitized user input +const template = (t) => function (params = {}) { + with (params) { + return eval(`\`${t}\``) + } +} + module.exports = { + isObject, + isEqual, emptyObj, debounce, difference, @@ -93,6 +125,7 @@ module.exports = { partial, partition, pick, + template, throttle, union, unionBy From 220dc3afd419d731700f9aa284a45a95a83daa91 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Wed, 1 Mar 2023 08:29:25 -0500 Subject: [PATCH 10/13] make sure the baseURL gets set correctly for the eventSource --- api/events.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/events.js b/api/events.js index bde620d..7f455ff 100644 --- a/api/events.js +++ b/api/events.js @@ -6,11 +6,13 @@ class MarathonEventSource { constructor (ctx) { // accepts a parent context this.parent = ctx this.baseURL = ctx.baseURL - this.baseURL.pathname = `${ctx.basePath}/events` + this.baseURL.pathname = this.path this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } + get path () { return `${this.parent.basePath}/events` } + // the constructor url should contain credentials and the api basepath '/v2' createEventSource (opts = {}) { const { eventType } = opts @@ -19,6 +21,7 @@ class MarathonEventSource { throw new Error('"eventType" should be an array or string') } this.baseURL.searchParams.set('event_type', eventType) + this.baseURL.pathname = this.path } this.es = new EventSource(`${this.baseURL}`) return this.es From 80741665a0f982ed002cf8a5610d134d5b760f43 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Wed, 1 Mar 2023 09:45:18 -0500 Subject: [PATCH 11/13] fix url search params for eventSource when filtering on event types --- api/events.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/api/events.js b/api/events.js index 7f455ff..8df551b 100644 --- a/api/events.js +++ b/api/events.js @@ -4,24 +4,27 @@ var EventSource = require('eventsource') class MarathonEventSource { constructor (ctx) { // accepts a parent context - this.parent = ctx + this.basePath = ctx.basePath this.baseURL = ctx.baseURL this.baseURL.pathname = this.path this.client = ctx.http this.client.defaults.baseURL = this.baseURL.toString() } - get path () { return `${this.parent.basePath}/events` } + get path () { return `${this.basePath}/events` } // the constructor url should contain credentials and the api basepath '/v2' createEventSource (opts = {}) { + this.baseURL.pathname = this.path // make sure we have a good path const { eventType } = opts if (eventType) { - if (!((eventType instanceof String) || (eventType instanceof Array))) { + if (typeof eventType === 'string') { + this.baseURL.searchParams.set('event_type', eventType) + } else if (eventType instanceof Array) { // array + for (const type of eventType) { this.baseURL.searchParams.append('event_type', type) } + } else { throw new Error('"eventType" should be an array or string') } - this.baseURL.searchParams.set('event_type', eventType) - this.baseURL.pathname = this.path } this.es = new EventSource(`${this.baseURL}`) return this.es From 5dd5c48bcf3a747b48a1667355c3e24e92e786e9 Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Wed, 1 Mar 2023 11:28:00 -0500 Subject: [PATCH 12/13] don't throw an error if we get undefined for the eventType --- api/events.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/events.js b/api/events.js index 8df551b..4073069 100644 --- a/api/events.js +++ b/api/events.js @@ -22,9 +22,7 @@ class MarathonEventSource { this.baseURL.searchParams.set('event_type', eventType) } else if (eventType instanceof Array) { // array for (const type of eventType) { this.baseURL.searchParams.append('event_type', type) } - } else { - throw new Error('"eventType" should be an array or string') - } + } // ignore anything that isn't a string or array } this.es = new EventSource(`${this.baseURL}`) return this.es From 51ee96968e345b8905f252606c039b4e02bed15d Mon Sep 17 00:00:00 2001 From: John Fanjoy Date: Wed, 1 Mar 2023 11:29:00 -0500 Subject: [PATCH 13/13] increment version to v2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64ad3fb..e9b0f87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marathon-node", - "version": "2.0.0", + "version": "2.0.1", "description": "Node.js client library for Mesos Marathon's REST API", "main": "index.js", "scripts": {