UNPKG

@webscale-networks/cloudedge-handlers

Version:

Webscale Networks CloudEDGE Handlers for cloud-agnostic edge function execution

211 lines (198 loc) 7.01 kB
'use strict'; const fs = require('fs'); const https = require('https'); const path = require('path'); const _ = require('lodash'); // Given a request, a response, and the handlers to run, this function runs // them. It returns an object that may include error, request and response keys. // If the handle cannot be parsed or located, the error key is set. exports.run = async (wRequest, wResponse, handles, config) => { const handle = this.parseHandles(handles); if (!handle) { const error = new Error(`Handle "${handles}" cannot be parsed.`); await this._log(error, config, wRequest); return this.error(error.message); } const location = this.locate( this.rootDirectory(module.paths.reverse()), handle.module, ); if (!location) { const error = new Error(`Module "${handle.module}" cannot be located.`); await this._log(error, config, wRequest); return this.error(error.message); } try { const handler = require(location); let result = {}; if (wResponse) { result = await handler[handle.func](wRequest, wResponse); } else { result = await handler[handle.func](wRequest); } if (_.get(result, 'error')) { await this._log(result.error, config, wRequest); } return result; } catch(error) { await this._log(error, config, wRequest); return this.error(error.message); } }; // Convenience function for creating an error. exports.error = (message) => { return { error: message, }; }; // Returns the relative path of the Javascript module from its configuration in // the project's manifest.json file. If the manifest file does not exist, // cannot be read or the module cannot be found, null is returned. Otherwise, // the path of the module relative to the root directory is returned. exports.locate = (rootDir, moduleName) => { try { const contents = fs.readFileSync(path.join(rootDir, 'manifest.json')); const manifest = JSON.parse(contents); const module = manifest.find((moduleMetadata) => { return moduleMetadata.moduleName == moduleName; }); if (module) { return path.join(rootDir, module.relativePath); } } catch(error) { console.error(error); } return null; }; // Parses strings of the form: // <module_name>.<function_name>, // Returns an object with the properties 'module' and 'func' if the string can // be parsed and null otherwise. exports.parseHandles = (handles) => { if (handles.length < 1) { return null; } // TODO: Only use the first handle for now. const tokenizedHandle = handles[0].trim().split('.'); if (tokenizedHandle.length == 2) { return { module: tokenizedHandle[0], func: tokenizedHandle[1], } } return null; }; // Given an array of paths, returns the first directory path that includes a // file named 'manifest.json'. If no path is found, null is returned. exports.rootDirectory = (paths) => { for (const modulePath of paths) { if (fs.existsSync( path.join(path.dirname(modulePath), 'manifest.json'), ) ) { return path.dirname(modulePath); } } return null; }; // Logs an error by writing it to the console and logging it in the Webscale // application's event logs. // // Do not use this directly. It is exported for unit testing only. exports._log = async (error, handlerConfig, wRequest) => { try { console.error(`Error running handler: ${error.message}:${error.stack}`); await this._postEventLog( this._createLog(this._errorContext(handlerConfig, wRequest, error)), handlerConfig.access_key, ); } catch(error) { console.error(`Error creating Webscale event log: ${error.message}.`); } }; // Creates an event log context. The context includes details for making clear, // readable Webscale event logs. // // Do not use this directly. It is exported for unit testing only. exports._errorContext = (handlerConfig, wRequest, error) => { let wsapp = null; let requestId = null; if (wRequest.providerSpecific) { wsapp = this._appReference(wRequest.originRequest.headers.get('wsapp')[0]); requestId = wRequest.providerSpecific.requestId; } else { wsapp = '/v2/applications/'+wRequest.wsapp; requestId = wRequest.requestId; } return { isProxyRequest: wRequest.providerSpecific == null, handler: handlerConfig['handler'], application: wsapp, requestAddress: wRequest.peerAddress, requestPath: wRequest.path, requestId: requestId, error: error, }; }; // Creates an event log consumable by Webscale's events API. // // Do not use this directly. It is exported for unit testing only. exports._createLog = (context) => { let msg = 'CloudEDGE worker raised an exception'; let type = 'cloudedge-worker-exception'; if (context.isProxyRequest) { msg = 'Serverless handler raised an exception'; type = 'serverless-handler-exception'; } return JSON.stringify({ subject: context.application, message: msg, severity: 'severe', type: type, details: { application: context.application, request_path: context.requestPath, request_address: context.requestAddress, request_id: context.requestId, handler: context.handler, error: { message: context.error.message, stack: context.error.stack, }, }, }); }; // Creates an event in the Webscale application's event logs via Webscale's // events API. There is no need to wait for the Webscale API to response so // the promise this function returns is resolved immediately after the full // API request has been sent. // // Do not use this directly. It is exported for unit testing only. exports._postEventLog = (log, accessKey) => { const options = { hostname: WS_API, port: 443, path: WS_EVENTS_ENDPOINT, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': log.length, 'Authorization': `Bearer ${accessKey}`, }, }; return new Promise((resolve, reject) => { const req = https.request(options); req.on('error', error => { reject(error) }); req.write(log); req.end(null, null, () => { resolve(req) }); }); }; // Returns the application reference given // '<app-name> (applications/<app-api-id)'. // // Do not use this directly. It is exported for unit testing only. exports._appReference = (app) => { return `/v2/${app.match(/\((.*)\)/)[1]}`; }; const WS_API = 'api.webscale.com'; const WS_EVENTS_ENDPOINT = '/v2/events';