From a98d2e7b9e1f26c0b1a9df841de8c2eee8fe40ab Mon Sep 17 00:00:00 2001 From: Trygve Lie Date: Thu, 27 Oct 2022 13:49:44 +0200 Subject: [PATCH] feat: Add plugin API --- lib/classes/http-incoming.js | 36 +++++++++++++++ lib/classes/plugin-demo-a.js | 38 +++++++++++++++ lib/classes/plugin-demo-b.js | 31 +++++++++++++ lib/classes/plugin.js | 26 +++++++++++ lib/handlers/pkg.get.js | 90 ++++++++++++++++++++++++++---------- lib/handlers/pkg.log.js | 81 ++++++++++++++++++++++++-------- lib/handlers/pkg.put.js | 75 +++++++++++++++++++++--------- 7 files changed, 311 insertions(+), 66 deletions(-) create mode 100644 lib/classes/plugin-demo-a.js create mode 100644 lib/classes/plugin-demo-b.js create mode 100644 lib/classes/plugin.js diff --git a/lib/classes/http-incoming.js b/lib/classes/http-incoming.js index fe2559cc..a0004f17 100644 --- a/lib/classes/http-incoming.js +++ b/lib/classes/http-incoming.js @@ -1,3 +1,5 @@ +import { validators } from '@eik/common'; + /** * A bearer object to hold misc data through a http * request / response cyclus. @@ -26,16 +28,30 @@ const HttpIncoming = class HttpIncoming { this._request = request; this._headers = request ? request.headers : {}; + + this._handle = ''; + } + + set version(value) { + this._version = validators.version(value); } get version() { return this._version; } + set extras(value) { + this._extras = validators.extra(value); + } + get extras() { return this._extras; } + set author(value) { + this._author = value; + } + get author() { return this._author; } @@ -44,14 +60,26 @@ const HttpIncoming = class HttpIncoming { return this._alias; } + set name(value) { + this._name = validators.name(value); + } + get name() { return this._name; } + set type(value) { + this._type = validators.type(value); + } + get type() { return this._type; } + set org(value) { + this._org = value; + } + get org() { return this._org; } @@ -64,6 +92,14 @@ const HttpIncoming = class HttpIncoming { return this._headers; } + set handle(value) { + this._handle = value; + } + + get handle() { + return this._handle; + } + get [Symbol.toStringTag]() { return 'HttpIncoming'; } diff --git a/lib/classes/plugin-demo-a.js b/lib/classes/plugin-demo-a.js new file mode 100644 index 00000000..feb41c36 --- /dev/null +++ b/lib/classes/plugin-demo-a.js @@ -0,0 +1,38 @@ +import Plugin from './plugin.js'; + +const PluginDemoA = class PluginDemoA extends Plugin { + constructor({ + name = '', + } = {}) { + super(); + this._name = name; + } + + get name() { + return this._name; + } + + onRequestStart(incoming) { + if (incoming.handle !== 'put:pkg:version') { + return; + } + + return new Promise((resolve, reject) => { + // console.log('PLUGIN A START', this._name, incoming.type, 'pkg:put:start'); + resolve(incoming); + }); + } + + onRequestEnd(incoming, outgoing) { + return new Promise((resolve, reject) => { + // console.log('PLUGIN A END', this._name, incoming.type, 'pkg:put:end'); + resolve(incoming); + }); + } + + get [Symbol.toStringTag]() { + return 'PluginDemoA'; + } +} + +export default PluginDemoA; \ No newline at end of file diff --git a/lib/classes/plugin-demo-b.js b/lib/classes/plugin-demo-b.js new file mode 100644 index 00000000..e1efbc08 --- /dev/null +++ b/lib/classes/plugin-demo-b.js @@ -0,0 +1,31 @@ +import Plugin from './plugin.js'; + +const PluginDemoB = class PluginDemoB extends Plugin { + constructor({ + name = '', + } = {}) { + super(); + this._name = name; + } + + get name() { + return this._name; + } + + onRequestStart(incoming) { + if (incoming.handle !== 'put:pkg:version') { + return false; + } + + return new Promise((resolve, reject) => { + // console.log('PLUGIN B START', this._name, incoming.type, 'pkg:put:start'); + resolve(incoming); + }); + } + + get [Symbol.toStringTag]() { + return 'PluginDemoB'; + } +} + +export default PluginDemoB; \ No newline at end of file diff --git a/lib/classes/plugin.js b/lib/classes/plugin.js new file mode 100644 index 00000000..616ea462 --- /dev/null +++ b/lib/classes/plugin.js @@ -0,0 +1,26 @@ +import abslog from 'abslog'; + +const Plugin = class Plugin { + constructor({ + logger, + } = {}) { + this._log = abslog(logger); + } + + onRequestStart() { + return undefined; + } + + onRequestEnd() { + return undefined; + } + + healthcheck() { + return undefined; + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } +} +export default Plugin; \ No newline at end of file diff --git a/lib/handlers/pkg.get.js b/lib/handlers/pkg.get.js index e09337ad..450921b8 100644 --- a/lib/handlers/pkg.get.js +++ b/lib/handlers/pkg.get.js @@ -1,4 +1,3 @@ -import { validators } from '@eik/common'; import originalUrl from 'original-url'; import HttpError from 'http-errors'; import abslog from 'abslog'; @@ -6,6 +5,7 @@ import Metrics from '@metrics/client'; import { createFilePathToAsset } from '../utils/path-builders-fs.js'; import { decodeUriComponent } from '../utils/utils.js'; +import HttpIncoming from '../classes/http-incoming.js'; import HttpOutgoing from '../classes/http-outgoing.js'; import Asset from '../classes/asset.js'; import config from '../utils/defaults.js'; @@ -14,6 +14,7 @@ const PkgGet = class PkgGet { constructor({ organizations, cacheControl, + plugins, logger, sink, etag, @@ -45,6 +46,7 @@ const PkgGet = class PkgGet { ], }); this._orgRegistry = new Map(this._organizations); + this._plugins = plugins || []; } get metrics() { @@ -53,16 +55,15 @@ const PkgGet = class PkgGet { async handler(req, type, name, version, extra) { const end = this._histogram.timer(); - - const pVersion = decodeUriComponent(version); - const pExtra = decodeUriComponent(extra); - const pName = decodeUriComponent(name); + const incoming = new HttpIncoming(req); + incoming.handle = `put:${type}:version`; try { - validators.version(pVersion); - validators.extra(pExtra); - validators.name(pName); - validators.type(type); + incoming.version = decodeUriComponent(version); + incoming.extras = decodeUriComponent(extra); + incoming.name = decodeUriComponent(name); + incoming.type = type; + } catch (error) { this._log.debug(`pkg:get - Validation failed - ${error.message}`); const e = new HttpError.NotFound(); @@ -71,30 +72,51 @@ const PkgGet = class PkgGet { } const url = originalUrl(req); - const org = this._orgRegistry.get(url.hostname); + incoming.org = this._orgRegistry.get(url.hostname); - if (!org) { + if (!incoming.org) { this._log.info(`pkg:get - Hostname does not match a configured organization - ${url.hostname}`); const e = new HttpError.InternalServerError(); end({ labels: { success: false, status: e.status, type } }); throw e; } + + + // Run On Request Start plugin methods + if (this._plugins.length !== 0) { + const pluginStart = this._plugins.map((plugin) => plugin.onRequestStart(incoming)).filter(plugin => plugin !== undefined); + + if (pluginStart.length !== 0) { + try { + await Promise.all(pluginStart); + } catch (error) { + this._log.info(`pkg:put - A plugin errored during on request start exection - ${error.message}`); + const e = new HttpError.InternalServerError(); + end({ labels: { success: false, status: e.status, type } }); + throw e; + } + } + } + + + const asset = new Asset({ - pathname: pExtra, - version: pVersion, - name: pName, - type, - org, + pathname: incoming.extras, + version: incoming.version, + name: incoming.name, + type: incoming.type, + org: incoming.org, }); const path = createFilePathToAsset(asset); + const outgoing = new HttpOutgoing(); + outgoing.cacheControl = this._cacheControl; + outgoing.mimeType = asset.mimeType; + try { const file = await this._sink.read(path); - const outgoing = new HttpOutgoing(); - outgoing.cacheControl = this._cacheControl; - outgoing.mimeType = asset.mimeType; if (this._etag) { outgoing.etag = file.etag; @@ -111,17 +133,37 @@ const PkgGet = class PkgGet { outgoing.stream = file.stream; } - this._log.debug(`pkg:get - Asset found - Pathname: ${path}`); - - end({ labels: { status: outgoing.statusCode, type } }); - - return outgoing; } catch (error) { this._log.debug(`pkg:get - Asset not found - Pathname: ${path}`); const e = new HttpError.NotFound(); end({ labels: { success: false, status: e.status, type } }); throw e; } + + + + // Run On Request End plugin methods + if (this._plugins.length !== 0) { + const pluginEnd = this._plugins.map((plugin) => plugin.onRequestEnd(incoming, outgoing)).filter(plugin => plugin !== undefined); + + if (pluginEnd.length !== 0) { + try { + await Promise.all(pluginEnd); + } catch (error) { + this._log.info(`pkg:put - A plugin errored during on request end exection - ${error.message}`); + const e = new HttpError.InternalServerError(); + end({ labels: { success: false, status: e.status, type } }); + throw e; + } + } + } + + + + this._log.debug(`pkg:get - Asset found - Pathname: ${path}`); + + end({ labels: { status: outgoing.statusCode, type } }); + return outgoing; } }; export default PkgGet; diff --git a/lib/handlers/pkg.log.js b/lib/handlers/pkg.log.js index 38f85798..48e1100d 100644 --- a/lib/handlers/pkg.log.js +++ b/lib/handlers/pkg.log.js @@ -1,4 +1,3 @@ -import { validators } from '@eik/common'; import originalUrl from 'original-url'; import HttpError from 'http-errors'; import abslog from 'abslog'; @@ -6,6 +5,7 @@ import Metrics from '@metrics/client'; import { createFilePathToPackage } from '../utils/path-builders-fs.js'; import { decodeUriComponent } from '../utils/utils.js'; +import HttpIncoming from '../classes/http-incoming.js'; import HttpOutgoing from '../classes/http-outgoing.js'; import config from '../utils/defaults.js'; @@ -13,6 +13,7 @@ const PkgLog = class PkgLog { constructor({ organizations, cacheControl, + plugins, logger, sink, etag, @@ -43,6 +44,7 @@ const PkgLog = class PkgLog { ], }); this._orgRegistry = new Map(this._organizations); + this._plugins = plugins || []; } get metrics() { @@ -51,14 +53,13 @@ const PkgLog = class PkgLog { async handler(req, type, name, version) { const end = this._histogram.timer(); - - const pVersion = decodeUriComponent(version); - const pName = decodeUriComponent(name); + const incoming = new HttpIncoming(req); + incoming.handle = `get:${type}:log`; try { - validators.version(pVersion); - validators.name(pName); - validators.type(type); + incoming.version = decodeUriComponent(version); + incoming.name = decodeUriComponent(name); + incoming.type = type; } catch (error) { this._log.debug(`pkg:log - Validation failed - ${error.message}`); const e = new HttpError.NotFound(); @@ -67,22 +68,43 @@ const PkgLog = class PkgLog { } const url = originalUrl(req); - const org = this._orgRegistry.get(url.hostname); + incoming.org = this._orgRegistry.get(url.hostname); - if (!org) { + if (!incoming.org) { this._log.info(`pkg:log - Hostname does not match a configured organization - ${url.hostname}`); const e = new HttpError.InternalServerError(); end({ labels: { success: false, status: e.status, type } }); throw e; } - const path = createFilePathToPackage({ org, type, name: pName, version: pVersion }); + + + // Run On Request Start plugin methods + if (this._plugins.length !== 0) { + const pluginStart = this._plugins.map((plugin) => plugin.onRequestStart(incoming)).filter(plugin => plugin !== undefined); + + if (pluginStart.length !== 0) { + try { + await Promise.all(pluginStart); + } catch (error) { + this._log.info(`pkg:put - A plugin errored during on request start exection - ${error.message}`); + const e = new HttpError.InternalServerError(); + end({ labels: { success: false, status: e.status, type } }); + throw e; + } + } + } + + + + const path = createFilePathToPackage(incoming); + + const outgoing = new HttpOutgoing(); + outgoing.cacheControl = this._cacheControl; + outgoing.mimeType = 'application/json'; try { const file = await this._sink.read(path); - const outgoing = new HttpOutgoing(); - outgoing.cacheControl = this._cacheControl; - outgoing.mimeType = 'application/json'; if (this._etag) { outgoing.etag = file.etag; @@ -98,18 +120,37 @@ const PkgLog = class PkgLog { outgoing.statusCode = 200; outgoing.stream = file.stream; } - - this._log.debug(`pkg:log - Package log found - Pathname: ${path}`); - - end({ labels: { status: outgoing.statusCode, type } }); - - return outgoing; } catch (error) { - this._log.debug(`pkg:log - Package log found - Pathname: ${path}`); + this._log.debug(`pkg:log - Package log not found - Pathname: ${path}`); const e = new HttpError.NotFound(); end({ labels: { success: false, status: e.status, type } }); throw e; } + + this._log.debug(`pkg:log - Package log found - Pathname: ${path}`); + + + + // Run On Request End plugin methods + if (this._plugins.length !== 0) { + const pluginEnd = this._plugins.map((plugin) => plugin.onRequestEnd(incoming, outgoing)).filter(plugin => plugin !== undefined); + + if (pluginEnd.length !== 0) { + try { + await Promise.all(pluginEnd); + } catch (error) { + this._log.info(`pkg:put - A plugin errored during on request end exection - ${error.message}`); + const e = new HttpError.InternalServerError(); + end({ labels: { success: false, status: e.status, type } }); + throw e; + } + } + } + + + + end({ labels: { status: outgoing.statusCode, type } }); + return outgoing; } }; export default PkgLog; diff --git a/lib/handlers/pkg.put.js b/lib/handlers/pkg.put.js index f06ae1fe..84a61d7f 100644 --- a/lib/handlers/pkg.put.js +++ b/lib/handlers/pkg.put.js @@ -1,4 +1,3 @@ -import { validators } from '@eik/common'; import originalUrl from 'original-url'; import HttpError from 'http-errors'; import Metrics from '@metrics/client'; @@ -24,6 +23,7 @@ const PkgPut = class PkgPut { pkgMaxFileSize, organizations, cacheControl, + plugins, logger, sink, } = {}) { @@ -59,6 +59,8 @@ const PkgPut = class PkgPut { legalFiles: ['package'], sink: this._sink, }); + + this._plugins = plugins || []; } get metrics() { @@ -146,16 +148,14 @@ const PkgPut = class PkgPut { } async handler(req, user, type, name, version) { - const end = this._histogram.timer(); - - const pVersion = decodeUriComponent(version); - const pName = decodeUriComponent(name); + const incoming = new HttpIncoming(req); + incoming.handle = `put:${type}:version`; try { - validators.version(pVersion); - validators.name(pName); - validators.type(type); + incoming.version = decodeUriComponent(version); + incoming.name = decodeUriComponent(name); + incoming.type = type; } catch (error) { this._log.info(`pkg:put - Validation failed - ${error.message}`); const e = new HttpError.BadRequest(); @@ -164,39 +164,51 @@ const PkgPut = class PkgPut { } const url = originalUrl(req); - const org = this._orgRegistry.get(url.hostname); + incoming.org = this._orgRegistry.get(url.hostname); - if (!org) { + if (!incoming.org) { this._log.info(`pkg:put - Hostname does not match a configured organization - ${url.hostname}`); const e = new HttpError.InternalServerError(); end({ labels: { success: false, status: e.status, type } }); throw e; } - const author = new Author(user); - - const incoming = new HttpIncoming(req, { - version: pVersion, - author, - type, - name: pName, - org, - }); + incoming.author = new Author(user); const versionExists = await this._readVersion(incoming); if (versionExists) { this._log.info( - `pkg:put - Semver version already exists for the package - Org: ${org} - Name: ${pName} - Version: ${pVersion}`, + `pkg:put - Semver version already exists for the package - Org: ${incoming.org} - Name: ${incoming.name} - Version: ${incoming.version}`, ); const e = new HttpError.Conflict(); end({ labels: { success: false, status: e.status, type } }); throw e; } + + + // Run On Request Start plugin methods + if (this._plugins.length !== 0) { + const pluginStart = this._plugins.map((plugin) => plugin.onRequestStart(incoming)).filter(plugin => plugin !== undefined); + + if (pluginStart.length !== 0) { + try { + await Promise.all(pluginStart); + } catch (error) { + this._log.info(`pkg:put - A plugin errored during on request start exection - ${error.message}`); + const e = new HttpError.InternalServerError(); + end({ labels: { success: false, status: e.status, type } }); + throw e; + } + } + } + + + const versions = await this._readVersions(incoming); const pkg = await this._parser(incoming); - versions.setVersion(pVersion, pkg.integrity); + versions.setVersion(incoming.version, pkg.integrity); try { await this._writeVersions(incoming, versions); @@ -211,8 +223,27 @@ const PkgPut = class PkgPut { outgoing.statusCode = 303; outgoing.location = createURIPathToPkgLog(pkg); - end({ labels: { status: outgoing.statusCode, type } }); + + // Run On Request End plugin methods + if (this._plugins.length !== 0) { + const pluginEnd = this._plugins.map((plugin) => plugin.onRequestEnd(incoming, outgoing)).filter(plugin => plugin !== undefined); + + if (pluginEnd.length !== 0) { + try { + await Promise.all(pluginEnd); + } catch (error) { + this._log.info(`pkg:put - A plugin errored during on request end exection - ${error.message}`); + const e = new HttpError.InternalServerError(); + end({ labels: { success: false, status: e.status, type } }); + throw e; + } + } + } + + + + end({ labels: { status: outgoing.statusCode, type } }); return outgoing; } };