@webscale-networks/cloudedge-handlers
Version:
Webscale Networks CloudEDGE Handlers for cloud-agnostic edge function execution
211 lines (198 loc) • 7.01 kB
JavaScript
;
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';