drp-mesh
Version:
1,178 lines (1,054 loc) • 208 kB
JavaScript
'use strict';
const os = require('os');
const fs = require('fs');
const util = require('util');
const tcpp = require('tcp-ping');
const tcpPing = util.promisify(tcpp.ping);
const ping = require('ping');
const dns = require('dns').promises;
const express = require('express');
const bodyParser = require('body-parser');
const DRP_Endpoint = require("./endpoint");
const DRP_Client = require("./client");
const DRP_Service = require("./service");
const { DRP_TopicManager, DRP_TopicManager_Topic } = require("./topicmanager");
const DRP_RouteHandler = require("./routehandler");
const { DRP_MethodParams, DRP_GetParams } = require("./params");
const { DRP_WebServer, DRP_WebServerConfig } = require("./webserver");
const { DRP_SubscribableSource, DRP_Subscriber } = require('./subscription');
const { DRP_AuthRequest, DRP_AuthResponse, DRP_AuthFunction, DRP_AuthInfo } = require('./auth');
const { DRP_Packet, DRP_Cmd, DRP_Reply, DRP_Stream, DRP_RouteOptions, DRP_CmdError, DRP_ErrorCode } = require('./packet');
const { DRP_Permission, DRP_PermissionSet, DRP_Securable, DRP_VirtualFunction, DRP_VirtualDirectory } = require('./securable');
const Express_Request = express.request;
const Express_Response = express.response;
const { v4: uuidv4 } = require('uuid');
const DRPVersion = "1.0.5"
class DRP_RemotePath {
/**
* Create a reference to a remote path
* @param {DRP_Node} localNode
* @param {string} targetNodeID
* @param {DRP_MethodParams} params
*/
constructor(localNode, targetNodeID) {
this.localNode = localNode;
this.targetNodeID = targetNodeID;
}
/**
* Send pathCmd to remote node
* @param {DRP_MethodParams} params
*/
async CallPath(params) {
let returnObj = await this.localNode.SendPathCmdToNode(this.targetNodeID, params);
return returnObj;
}
}
class DRP_NodeDeclaration {
/**
*
* @param {string} nodeID Node ID
* @param {string[]} nodeRoles Functional Roles ['Registry','Broker','Portal','Provider','Producer','Sidecar','Logger']
* @param {string} hostID Host Identifier
* @param {string} nodeURL Listening URL (optional)
* @param {string} domainName Domain Name
* @param {string} meshKey Domain Key
* @param {string} zoneName Zone Name
* @param {string} scope Scope
*/
constructor(nodeID, nodeRoles, hostID, nodeURL, domainName, meshKey, zoneName, scope) {
this.NodeID = nodeID;
this.NodeRoles = nodeRoles;
this.HostID = hostID;
this.NodeURL = nodeURL;
this.DomainName = domainName;
this.MeshKey = meshKey;
this.Zone = zoneName;
this.Scope = scope;
}
}
class DRP_Node extends DRP_Securable {
#MeshKey
#mTLS
#debug
#registrySet
#useSwagger
#rejectUnreachable
/**
*
* @param {string[]} nodeRoles Functional Roles ['Registry','Broker','Provider','Producer','Logger']
* @param {string} hostID Host Identifier
* @param {string} domainName DRP Domain Name
* @param {string} meshKey DRP Mesh Key
* @param {string} zone DRP Zone Name (optional)
* @param {DRP_WebServerConfig} webServerConfig Web server config (optional)
* @param {string} drpRoute DRP WS Route (optional)
* @param {DRP_PermissionSet} permissionSet DRP Permission Set
*/
constructor(nodeRoles, hostID, domainName, meshKey, zone, webServerConfig, drpRoute, permissionSet) {
super(permissionSet);
let thisNode = this;
this.NodeID = `${os.hostname()}-${process.pid}`;
this.HostID = hostID;
/** @type {DRP_WebServer} */
this.WebServer = null;
this.WebServerConfig = webServerConfig || null;
this.drpRoute = drpRoute || "/";
this.DomainName = domainName;
this.#MeshKey = meshKey || null;
this.#mTLS = null;
//if (!this.#MeshKey) this.Die("No MeshKey provided");
this.Zone = zone || "default";
/** @type{string} */
this.RegistryUrl = null;
this.Debug = false;
this.RegistrySet = null;
this.UseSwagger = false;
/** @type{string} */
this.AuthenticationServiceName = null;
this.HasConnectedToMesh = false;
/** @type{function} */
this.onControlPlaneConnect = null;
this.ListeningURL = null;
this.PendingRegistryConnections = new Set();
/** @type{string[]} */
this.NodeRoles = nodeRoles || [];
/** @type{string} */
this.WebProxyURL = null;
// If we have a web server config, start listening
if (this.WebServerConfig && this.WebServerConfig.ListeningURL) {
this.WebServer = new DRP_WebServer(webServerConfig);
this.WebServer.start();
this.ListeningURL = this.WebServer.config.ListeningURL;
}
// By default, Registry nodes are "connected" to the Control Plane and non-Registry nodes aren't
this.isConnectedToControlPlane = thisNode.IsRegistry();
// True if this node is currently attempting to connect to the control plane
this.isConnectingToControlPlane = false;
// Wait time for Registry reconnect attempts
this.ReconnectWaitTimeSeconds = 0;
/** @type {Object.<string,DRP_NodeClient>} */
this.NodeEndpoints = {};
/** @type {Object.<string,DRP_NodeClient>} */
this.ConsumerEndpoints = {};
// Create topic manager - Handles stream messaging
this.TopicManager = new DRP_TopicManager(thisNode);
// Create subscription manager - Handles client subscriptions
this.SubscriptionManager = new DRP_SubscriptionManager(thisNode);
// Create topology tracker - Processes changes in mesh topology
this.TopologyTracker = new DRP_TopologyTracker(thisNode);
// Add this node to TopologyTracker
let newNodeEntry = new DRP_NodeTableEntry(thisNode.NodeID, null, nodeRoles, this.ListeningURL, "global", this.Zone, this.NodeID, this.HostID);
let addNodePacket = new DRP_TopologyPacket(newNodeEntry.NodeID, "add", "node", newNodeEntry.NodeID, newNodeEntry.Scope, newNodeEntry.Zone, newNodeEntry);
thisNode.TopologyTracker.ProcessPacket(addNodePacket, this.NodeID);
/** @type Object.<string,DRP_Service> */
this.Services = {};
/** @type Object.<string,DRP_AuthResponse> */
this.ConsumerTokens = {};
// Add a route handler even if we don't have an Express server (needed for stream relays)
this.RouteHandler = new DRP_RouteHandler(this, this.drpRoute);
this.PacketRelayCount = 0;
this.TCPPing = this.TCPPing;
this.GetServiceDefinitions = this.GetServiceDefinitions;
this.ListClassInstances = this.ListClassInstances;
this.ToggleDebug = this.ToggleDebug;
this.GetDebug = () => { return this.Debug; }
this.Evacuate = this.Evacuate;
this.Mesh = async (params, callingEndpoint) => {
let pathData = await thisNode.PathCmd(params, thisNode.GetBaseObj().Mesh, callingEndpoint);
return pathData;
}
let localDRPEndpoint = new DRP_Endpoint(null, this, "Local");
this.ApplyNodeEndpointMethods(localDRPEndpoint);
let DRPService = new DRP_Service("DRP", this, "DRP", null, false, 10, 10, this.Zone, "local", null, ["Console", "TopologyTracker"], 1, DRPVersion);
DRPService.ClientCmds = localDRPEndpoint.EndpointCmds;
DRPService.Streams['Console'].MaxHistoryLength = 1000;
DRPService.Streams['TopologyTracker'].MaxHistoryLength = 1000;
// Add hook for watching topology for Subscription Manager
let topologySubscription = new DRP_Subscriber("DRP", "TopologyTracker", "local", null, null, false, (topologyPacket) => {
this.SubscriptionManager.ProcessTopologyPacket(topologyPacket.Message);
}, null);
this.TopicManager.SubscribeToTopic(topologySubscription);
this.AddService(DRPService);
}
/** @type boolean */
get Debug() { return this.#debug }
set Debug(val) { this.#debug = this.IsTrue(val) }
/** @type Object.<string,Object> */
get RegistrySet() { return this.#registrySet }
set RegistrySet(val) {
if (!val) {
this.#registrySet = null;
return;
}
this.#registrySet = val.split(/,/).reduce((map, registryEntry) => {
let [host, port] = registryEntry.split(/:/);
if (!host || !port) {
throw new Error(`Invalid registry entry [${registryEntry}], expecting "{host}:{port}"`);
}
let key = `${host}-${port}`;
map[key] = {
name: host,
port: port,
pingInfo: null
};
return map;
}, {});
}
get mTLS() { return this.#mTLS }
set mTLS(val) {
if (!val || !val.certFile || !val.keyFile) {
this.#mTLS = null;
return;
}
this.#mTLS = {
cert: fs.readFileSync(val.certFile),
key: fs.readFileSync(val.keyFile)
}
}
/** @type boolean */
get UseSwagger() { return this.#useSwagger }
set UseSwagger(val) { this.#useSwagger = this.IsTrue(val) }
/** @type boolean */
get RejectUnreachable() { return this.#rejectUnreachable }
set RejectUnreachable(val) { this.#rejectUnreachable = this.IsTrue(val) }
/**
* Print message to stdout
* @param {string} message Message to output
* @param {boolean} isDebugMsg Is it a debug message?
*/
log(message, isDebugMsg) {
// If it's a debug message and we don't have debugging turned on, return
if (!this.Debug && isDebugMsg) return;
let paddedNodeID = this.NodeID.padEnd(14, ' ');
let outputMsg = `${this.getTimestamp()} [${paddedNodeID}] -> ${message}`;
console.log(outputMsg);
if (this.TopicManager) {
this.TopicManager.SendToTopic("DRP", "Console", outputMsg);
}
}
getTimestamp() {
let date = new Date();
let hour = date.getHours();
hour = (hour < 10 ? "0" : "") + hour;
let min = date.getMinutes();
min = (min < 10 ? "0" : "") + min;
let sec = date.getSeconds();
sec = (sec < 10 ? "0" : "") + sec;
let ms = date.getMilliseconds();
if (ms < 10) {
ms = "00" + ms;
} else if (ms < 100) {
ms = "0" + ms;
}
let year = date.getFullYear();
let month = date.getMonth() + 1;
month = (month < 10 ? "0" : "") + month;
let day = date.getDate();
day = (day < 10 ? "0" : "") + day;
return year + "" + month + "" + day + "" + hour + "" + min + "" + sec + "." + ms;
}
ToggleDebug() {
if (this.Debug) {
this.Debug = false;
} else {
this.Debug = true;
}
return this.Debug;
}
__GetNodeDeclaration() {
let thisNode = this;
return new DRP_NodeDeclaration(thisNode.NodeID, thisNode.NodeRoles, thisNode.HostID, thisNode.ListeningURL, thisNode.DomainName, thisNode.#MeshKey, thisNode.Zone);
}
async GetConsumerToken(username, password) {
let thisNode = this;
let returnToken = null;
/** @type {DRP_AuthResponse} */
let authResults = await thisNode.Authenticate(username, password, null);
if (authResults) {
thisNode.ConsumerTokens[authResults.Token] = authResults;
// If this Node is a Broker, relay to other Brokers in Zone
if (thisNode.IsBroker()) {
let nodeIDList = Object.keys(thisNode.TopologyTracker.NodeTable);
for (let i = 0; i < nodeIDList.length; i++) {
let checkNode = thisNode.TopologyTracker.NodeTable[nodeIDList[i]];
if (checkNode.NodeID !== thisNode.NodeID && checkNode.IsBroker() && checkNode.Zone === thisNode.Zone) {
// Send command to remote Broker
thisNode.ServiceCmd("DRP", "addConsumerToken", { tokenPacket: authResults }, {
targetNodeID: checkNode.NodeID,
sendOnly: true
});
}
}
}
returnToken = authResults.Token;
}
return returnToken;
}
async GetConsumerTokenAnon() {
let thisNode = this;
let returnToken = null;
/** @type {DRP_AuthResponse} */
let authResults = new DRP_AuthResponse(uuidv4(), "Anonymous", "Anonymous", [], null, "Anonymous", thisNode.getTimestamp());
thisNode.ConsumerTokens[authResults.Token] = authResults;
returnToken = authResults.Token;
return returnToken;
}
GetLastTokenForUser(userName) {
let thisNode = this;
let tokenList = Object.keys(thisNode.ConsumerTokens).reverse();
for (let i = 0; i < tokenList.length; i++) {
let checkTokenID = tokenList[i];
let thisToken = thisNode.ConsumerTokens[tokenList[i]];
if (userName === thisToken.UserName) return checkTokenID;
}
return null;
}
/**
*
* @param {string} restRoute Route to listen for Node REST requests
* @param {string} basePath Base path list
* @param {boolean} writeToLogger If true, output REST Logs to Logger
* @returns {number} Failure code
*/
EnableREST(webServer, restRoute, basePath, writeToLogger) {
let thisNode = this;
if (!webServer || !webServer.expressApp) return 1;
// Set Consumer Token Cleanup Interval - 60 seconds
let checkFrequencyMs = 60000;
setInterval(() => {
let iCurrentTimestamp = parseInt(thisNode.getTimestamp());
let consumerTokenIDList = Object.keys(thisNode.ConsumerTokens);
// Collect tokens from ConsumerEndpoints
let connectedTokenList = [];
let consumerEndpointIDList = Object.keys(thisNode.ConsumerEndpoints);
for (let i = 0; i < consumerEndpointIDList.length; i++) {
let consumerEndpointID = consumerEndpointIDList[i];
connectedTokenList.push(thisNode.ConsumerEndpoints[consumerEndpointID].AuthInfo.value);
}
// TO DO - if clients automatically time out, we need to add a keepalive so that
// tokens of consumers connected to this Broker are not removed from other Brokers
for (let i = 0; i < consumerTokenIDList.length; i++) {
let checkToken = consumerTokenIDList[i];
let checkTokenObj = thisNode.ConsumerTokens[checkToken];
let iCheckTimestamp = parseInt(checkTokenObj.AuthTimestamp);
// Skip if currently connected
if (connectedTokenList.includes(checkToken)) continue;
// Expire after 30 minutes
let maxAgeSeconds = 60 * 30;
if (iCurrentTimestamp > iCheckTimestamp + maxAgeSeconds) {
// The token has expired
delete thisNode.ConsumerTokens[checkToken];
}
}
}, checkFrequencyMs);
let tmpBasePath = basePath || "";
let basePathArray = tmpBasePath.replace(/^\/|\/$/g, '').split('/');
// Get a list of valid HTTP codes, convert to numeric values for later comparison
let validHttpCodes = Object.keys(require('http').STATUS_CODES).map(statusCode => parseInt(statusCode));
/**
*
* @param {Express_Request} req Request
* @param {Express_Response} res Response
* @param {function} next Next step
*/
let nodeRestHandler = async function (req, res, next) {
OUTERTRY:
try {
// Get Auth Key
let authInfo = {
type: null,
value: null
};
// Check for authInfo:
// x-api-key - apps (static)
// x-api-token - users (dynamic)
let xapikey = null;
let xapitoken = null;
if (req.headers['x-api-key']) {
xapikey = req.headers['x-api-key'];
} else if (req.query['x-api-key']) {
xapikey = req.query['x-api-key'];
} else if (req.headers.cookie && /^x-api-key=.*$/.test(req.headers.cookie)) {
xapikey = req.headers.cookie.match(/^x-api-key=(.*)$/)[1];
} else if (req.headers['x-api-token']) {
xapitoken = req.headers['x-api-token'];
} else if (req.headers.cookie && /^x-api-token=.*$/.test(req.headers.cookie)) {
xapitoken = req.headers.cookie.match(/^x-api-token=(.*)$/)[1];
} else if (req.headers['x-gitlab-token']) {
xapikey = req.headers['x-gitlab-token']
}
if (thisNode.IsSidecar()) {
// No auth required from client, pass through sidecar creds
authInfo.type = 'sidecar';
authInfo.value = null;
} else if (xapikey) {
authInfo.type = 'key';
authInfo.value = xapikey;
} else if (xapitoken) {
authInfo.type = 'token';
authInfo.value = xapitoken;
// Make sure the token is current
if (!thisNode.ConsumerTokens[authInfo.value]) {
// We don't recognize this token
res.status(401).send("Invalid token");
return;
}
authInfo.userInfo = thisNode.ConsumerTokens[authInfo.value];
} else {
// Unauthorized
res.status(401).send("No x-api-token or x-api-key provided");
return;
}
// Turn path into list, remove first element
let decodedPath = decodeURIComponent(req.path);
let remainingPath = decodedPath.replace(/^\/|\/$/g, '').split('/');
remainingPath.shift();
// Init vars
let format = null;
let verb = null;
let resultString = "";
let resCode = 200;
// Reserved - format JSON output
if (req.query.__format) format = thisNode.IsTrue(req.query.__format);
// Reserved - override verb
if (req.query.__verb) verb = req.query.__verb;
// HTTP Verb Map
let httpMethodToVerbMap = {
GET: "GetItem",
POST: "SetItem",
PUT: "SetItem",
DELETE: "RemoveItem"
};
RUNTRY:
try {
// Client did not specify verb
if (!verb) {
// Get verb from HTTP method
verb = httpMethodToVerbMap[req.method];
if (!verb) {
resultString = `Invalid method: ${req.method}`;
resCode = DRP_ErrorCode.BADREQUEST;
break RUNTRY;
}
}
let params = new DRP_MethodParams(verb, basePathArray.concat(remainingPath), req.body, req.query, "REST", authInfo);
let resultObj = await thisNode.PathCmd(params, thisNode.GetBaseObj());
try {
// The res.send() method cannot accept numbers. Convert the result if it's not already a string.
let objectType = typeof resultObj;
switch (objectType) {
case "string":
resultString = resultObj;
break;
case "number":
case "boolean":
resultString = resultObj.toString();
break;
default:
resultString = JSON.stringify(resultObj, null, format);
}
} catch {
resultString = "Could not convert result to string";
resCode = DRP_ErrorCode.BADREQUEST;
}
} catch (ex) {
// An exception was thrown at some point in the call. If the code isn't a valid HTTP code, use 500.
if (validHttpCodes.includes(ex.code)) {
resCode = ex.code;
} else {
resCode = 500;
}
resultString = ex.message;
}
// Send response to client
res.status(resCode).send(resultString);
// Create message for logging
let logMessage = {
req: {
hostname: req.hostname,
ip: req.ip,
method: req.method,
protocol: req.protocol,
path: req.path,
headers: Object.assign({}, req.headers),
query: req.query,
baseUrl: req.baseUrl,
body: req.body
},
res: {
code: resCode,
length: resultString.length
}
};
// Remove authorization header for security purposes
delete logMessage.req.headers["authorization"];
// Send log to RESTLogs topic
thisNode.TopicManager.SendToTopic("DRP", "RESTLogs", logMessage);
// Write to Logger service if flag is set
if (writeToLogger) {
thisNode.ServiceCmd("Logger", "writeLog", { serviceName: "REST", logData: logMessage }, {
sendOnly: true
});
}
return;
} catch (ex) {
console.log(`Could not respond to REST request:`);
console.dir(ex);
console.dir({
url: req.url,
method: req.method,
headers: req.headers,
body: req.body,
ip: req.ip
});
}
};
// Assign REST handler
webServer.expressApp.all(`${restRoute}`, bodyParser.urlencoded({ extended: true }), bodyParser.json(), nodeRestHandler);
webServer.expressApp.all(`${restRoute}/*`, bodyParser.urlencoded({ extended: true }), bodyParser.json(), nodeRestHandler);
// Get Token
webServer.expressApp.post('/token', async (req, res) => {
// Get an auth token
let username = req.body.username;
let password = req.body.password;
let userToken = await GetConsumerToken(username, password);
if (userToken) {
res.send(`x-api-token: ${userToken}`);
} else {
res.status(401).send("Bad credentials");
}
return;
});
return 0;
}
/**
* Return a dictionary of class names, services and providers
* @param {DRP_MethodParams} params Parameters
* @param {object} callingEndpoint Calling Endpoint
* @param {token} token Command token
* @returns {Object.<string,Object.<string,{providers:string[]}>>} Class instances
*/
async ListClassInstances(params, callingEndpoint, token) {
let thisNode = this;
let results = {};
let findClassName = null;
if (params && params.className) findClassName = params.className;
else if (params && params.__pathList && params.__pathList.length > 0) findClassName = params.__pathList.shift();
// Get best instance for each service
let serviceDefinitions = await thisNode.GetServiceDefinitions(params, callingEndpoint);
// Get best instance of each service
let serviceList = Object.keys(serviceDefinitions);
for (let i = 0; i < serviceList.length; i++) {
let serviceName = serviceList[i];
let serviceInstanceObj = serviceDefinitions[serviceName];
let classList = Object.keys(serviceInstanceObj.Classes);
for (let k = 0; k < classList.length; k++) {
let className = classList[k];
if (!findClassName || findClassName === className) {
if (!results[className]) {
results[className] = {};
}
if (!results[className][serviceName]) {
results[className][serviceName] = { providers: [] };
}
results[className][serviceName].providers.push(serviceInstanceObj.NodeID);
}
}
}
return results;
}
GetServiceDefinition(params) {
/*
* We need to return:
* {
* "TestService": {ClientCmds: {}, Classes:{}, Streams:{}}
* }
*/
let thisNode = this;
let targetService = thisNode.Services[params.serviceName];
let serviceDefinition = targetService.GetDefinition();
return serviceDefinition;
}
async GetServiceDefinitions(params, callingEndpoint) {
/*
* We need to return:
* {
* "TestService": {ClientCmds: {}, Classes:{}, Streams:{}}
* }
*/
let thisNode = this;
let serviceDefinitions = {};
let serviceNameList = thisNode.TopologyTracker.ListServices();
for (let i = 0; i < serviceNameList.length; i++) {
let serviceName = serviceNameList[i];
// Unlike before, we don't distribute the actual service definitions in
// the declarations. We need to go out to each service and get the info
let bestInstance = thisNode.TopologyTracker.FindInstanceOfService(serviceName);
let response = await thisNode.ServiceCmd("DRP", "getServiceDefinition", { serviceName: serviceName }, {
targetNodeID: bestInstance.NodeID,
targetServiceInstanceID: bestInstance.InstanceID,
useControlPlane: true,
callingEndpoint: callingEndpoint
});
response.NodeID = bestInstance.NodeID;
serviceDefinitions[serviceName] = response;
}
return serviceDefinitions;
}
/**
* Get service definitions for this DRP_Node
* @param {DRP_MethodParams} params
* @param {any} callingEndpoint
*/
GetLocalServiceDefinitions(params, callingEndpoint) {
/*
* We need to return:
* {
* "TestService": {ClientCmds: {}, Classes:{}, Streams:{}}
* }
*/
let thisNode = this;
let serviceDefinitions = {};
let checkServiceName = null;
if (params) {
if (params.serviceName) checkServiceName = params.serviceName;
else if (params.__pathList && params.__pathList.length > 0) checkServiceName = params.__pathList.shift();
}
let serviceNameList = Object.keys(thisNode.Services);
for (let i = 0; i < serviceNameList.length; i++) {
let serviceName = serviceNameList[i];
if (checkServiceName && checkServiceName !== serviceName) continue;
let serviceDefinition = thisNode.Services[serviceName].GetDefinition();
serviceDefinitions[serviceName] = serviceDefinition;
}
return serviceDefinitions;
}
/**
* Return class records
* @param {DRP_MethodParams} params Parameters
* @returns {Object} Class records
*/
async GetClassRecords(params) {
let thisNode = this;
let results = {};
// If user didn't supply the className, return null
if (!params || !params.className) return null;
let thisClassName = params.className;
// We need to get a list of all distinct INSTANCES for this class along with the best source for each
let classInstances = await thisNode.ListClassInstances(params);
// If we don't have data for this class, return null
if (!classInstances[thisClassName]) return null;
let thisClassObj = classInstances[thisClassName];
// Loop over Class for this service
let serviceNames = Object.keys(thisClassObj);
for (let j = 0; j < serviceNames.length; j++) {
let serviceName = serviceNames[j];
if (params.serviceName && params.serviceName !== serviceName) continue;
let recordPath = ["Mesh", "Services", serviceName, "Classes", thisClassName, "cache"];
// Get data
let pathParams = new DRP_MethodParams("GetItem", recordPath);
let pathData = await thisNode.PathCmd(pathParams, thisNode.GetBaseObj());
results[serviceName] = pathData;
}
return results;
}
/**
* Send PathCmd to Remote Node
* @param {string} targetNodeID
* @param {Object} params
* @returns {any}
*/
async SendPathCmdToNode(targetNodeID, params) {
let thisNode = this;
let oReturnObject = null;
if (targetNodeID === thisNode.NodeID) {
// The target NodeID is local
oReturnObject = thisNode.PathCmd(params, thisNode.GetBaseObj());
} else {
oReturnObject = await thisNode.ServiceCmd("DRP", "pathCmd", params, {
targetNodeID: targetNodeID
});
}
return oReturnObject;
}
GetBaseObj() {
let thisNode = this;
let pathFuncs = {
Nodes: {
/**
* Get dictionary of available nodes in Mesh, override type as DRP_RemotePath
* @param {DRP_MethodParams} params
* @param {string} zoneName
*/
List: async (params, zoneName) => {
let nodeList = thisNode.TopologyTracker.ListNodes(zoneName);
let returnObj = nodeList.reduce((map, nodeID) => {
map[nodeID] = new DRP_RemotePath();
return map;
}, {});
return returnObj;
},
/**
* Find node and return a DRP_RemotePath command object
* @param {DRP_MethodParams} params
* @param {string} zoneName
*/
Get: async (params, zoneName) => {
let targetNodeID = params.__pathList.shift();
let checkEntry = thisNode.TopologyTracker.NodeTable[targetNodeID];
if (!checkEntry) {
throw new DRP_CmdError(`Node [${targetNodeID}] does not exist`, DRP_ErrorCode.NOTFOUND, "PathCmd");
}
if (zoneName && checkEntry.Zone !== zoneName) {
throw new DRP_CmdError(`Node [${targetNodeID}] is in Zone [${checkEntry.Zone}], not Zone [${zoneName}]`, DRP_ErrorCode.NOTFOUND, "PathCmd");
}
let returnObj = {}
returnObj = new DRP_RemotePath(thisNode, targetNodeID, params);
return returnObj;
},
},
Services: {
/**
* Get dictionary of available services in Mesh, override type as DRP_RemotePath
* @param {DRP_MethodParams} params
* @param {string} zoneName
*/
List: async (params, zoneName) => {
let serviceList = Object.keys(thisNode.TopologyTracker.GetServicesWithProviders(zoneName));
let returnObj = serviceList.reduce((map, serviceName) => {
map[serviceName] = new DRP_RemotePath();
return map;
}, {});
return returnObj;
},
/**
* Find an instance of the service and send a command to that path
* @param {DRP_MethodParams} params
* @param {string} zoneName
*/
Get: async (params, zoneName) => {
let serviceName = params.__pathList.shift();
params.__pathList = ['Services', serviceName].concat(params.__pathList);
let limitScope = zoneName ? "zone" : null;
let targetServiceEntry = thisNode.TopologyTracker.FindInstanceOfService(serviceName, null, zoneName, null, limitScope);
if (!targetServiceEntry) {
throw new DRP_CmdError("Service not found", DRP_ErrorCode.NOTFOUND, "PathCmd");
};
let targetNodeID = targetServiceEntry.NodeID;
let returnObj = {}
returnObj = new DRP_RemotePath(thisNode, targetNodeID, params);
return returnObj;
}
},
Streams: {
/**
* List available streams in Mesh
* @param {DRP_MethodParams} params
* @param {string} zoneName
*/
List: async (params, zoneName) => {
let returnObj = {};
let streamProviders = thisNode.TopologyTracker.GetStreamsWithProviders(zoneName);
for (let streamName in streamProviders) {
returnObj[streamName] = {}
streamProviders[streamName].reduce((map, streamName) => {
let newParams = Object.assign({}, params);
newParams.__pathList = ['DRPNode', 'TopicManager', 'Topics', streamName].concat(params.__pathList.slice(1));
map[streamName] = new DRP_RemotePath(thisNode, streamName, newParams);
return map;
}, returnObj[streamName]);
}
return returnObj;
},
/**
* Get dictionary of available services in Mesh, override type as DRP_RemotePath
* @param {DRP_MethodParams} params
* @param {string} zoneName
*/
Get: async (params, zoneName) => {
let streamName = params.__pathList.shift();
let returnObj = {};
let streamProviders = thisNode.TopologyTracker.GetStreamsWithProviders(zoneName);
if (!streamProviders[streamName]) {
throw new DRP_CmdError(`Stream [${streamName}] not found`, DRP_ErrorCode.NOTFOUND, "PathCmd");
}
returnObj = streamProviders[streamName].reduce((map, providerNodeID) => {
let newParams = Object.assign({}, params);
newParams.__pathList = ['DRPNode', 'TopicManager', 'Topics', streamName].concat(params.__pathList.slice(1));
map[providerNodeID] = new DRP_RemotePath(thisNode, providerNodeID, newParams);
return map;
}, returnObj);
return returnObj;
}
}
}
return {
NodeID: thisNode.NodeID,
NodeURL: thisNode.ListeningURL,
DRPNode: thisNode,
Services: thisNode.Services,
Streams: thisNode.TopicManager.Topics,
Endpoints: {
/**
* Return Node endpoints
* @param {DRP_MethodParams} params
*/
Nodes: async function (params) {
let remainingChildPath = params.__pathList;
let oReturnObject = null;
if (remainingChildPath && remainingChildPath.length > 0) {
let targetNodeID = remainingChildPath.shift();
// Need to send command to remote Node with remaining tree data
params.__pathList = remainingChildPath;
oReturnObject = await thisNode.SendPathCmdToNode(targetNodeID, params);
} else {
// Return list of NodeEndpoints
oReturnObject = {};
let aNodeKeys = Object.keys(thisNode.NodeEndpoints);
for (let i = 0; i < aNodeKeys.length; i++) {
oReturnObject[aNodeKeys[i]] = {
"ConsumerType": "SomeType1",
"Status": "Unknown"
};
}
}
return oReturnObject;
},
/**
* Return Consumer endpoints
* @param {DRP_MethodParams} params
*/
Consumers: async function (params) {
let remainingChildPath = params.__pathList;
let oReturnObject = null;
if (remainingChildPath && remainingChildPath.length > 0) {
let agentID = remainingChildPath.shift();
// Need to send command to consumer with remaining tree data
params.__pathList = remainingChildPath;
let targetEndpoint = await thisNode.VerifyConsumerConnection(agentID);
if (targetEndpoint) {
// Await for command from consumer
let results = await targetEndpoint.SendCmd("DRP", "pathCmd", params, true, null);
if (results) {
oReturnObject = results;
}
} else {
thisNode.log(`Could not verify consumer connection for [${agentID}]`, true);
}
} else {
// Return list of consumers
oReturnObject = {};
let aConsumerKeys = Object.keys(thisNode.ConsumerEndpoints);
for (let i = 0; i < aConsumerKeys.length; i++) {
oReturnObject[aConsumerKeys[i]] = {
"ConsumerType": "SomeType1",
"Status": "Unknown"
};
}
}
return oReturnObject;
}
},
Mesh: {
Topology: thisNode.TopologyTracker,
Nodes: new DRP_VirtualDirectory(
/**
* List Nodes
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Nodes.List(params);
},
/**
* Get Node
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Nodes.Get(params);
},
// Permission set
null
),
Services: new DRP_VirtualDirectory(
/**
* List Services
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Services.List(params);
},
/**
* Get Service
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Services.Get(params);
},
// Permission set
null
),
Streams: new DRP_VirtualDirectory(
/**
* List Streams
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Streams.List(params);
},
/**
* Get Stream
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Streams.Get(params);
},
// Permission set
null
),
Zones: new DRP_VirtualDirectory(
/**
* List Zones
* @param {DRP_MethodParams} params
*/
async (params) => {
// Return list of Zones
let returnObj = thisNode.TopologyTracker.ListZones().reduce((map, zoneName) => {
map[zoneName] = new DRP_RemotePath();
return map;
}, {});
return returnObj;
},
/**
* Get Zone
* @param {DRP_MethodParams} params
*/
async (params) => {
let returnObj = {};
// Get Zone
let zoneName = params.__pathList.shift();
// Make sure zone exists
if (!(thisNode.TopologyTracker.ListZones().includes(zoneName))) {
throw new DRP_CmdError(`Zone [${zoneName}] not found`, DRP_ErrorCode.NOTFOUND, "PathCmd");
}
// If the local DRPNode is not in the specified zone, we need to route the call to a Registry in the target zone
if (zoneName !== thisNode.Zone) {
let targetZoneRegistries = thisNode.TopologyTracker.FindRegistriesInZone(zoneName);
if (targetZoneRegistries.length == 0) {
throw new DRP_CmdError('No registries for zone', DRP_ErrorCode.UNAVAILABLE, "PathCmd");
}
let targetNodeID = targetZoneRegistries[0].NodeID;
params.__pathList = ['Mesh', 'Zones', zoneName].concat(params.__pathList);
returnObj = new DRP_RemotePath(thisNode, targetNodeID, params);
return returnObj;
}
// The local DRPNode is in the specified zone, proceed
let levelDefinitions = {
Nodes: new DRP_VirtualDirectory(
/**
* List Nodes
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Nodes.List(params, zoneName);
},
/**
* Get Node
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Nodes.Get(params, zoneName);
},
// Permission set
null
),
Services: new DRP_VirtualDirectory(
/**
* List Services
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Services.List(params, zoneName);
},
/**
* Get Service
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Services.Get(params, zoneName);
},
// Permission set
null
),
Streams: new DRP_VirtualDirectory(
/**
* List Streams
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Streams.List(params, zoneName);
},
/**
* Get Stream
* @param {DRP_MethodParams} params
*/
async (params) => {
return await pathFuncs.Streams.Get(params, zoneName);
},
// Permission set
null
)
}
returnObj = new DRP_VirtualDirectory(
/**
* List items
* @param {DRP_MethodParams} params
*/
async (params) => {
// Return list of target types in Zone
let returnObj = Object.keys(levelDefinitions).reduce((map, targetType) => {
map[targetType] = new DRP_VirtualDirectory();
return map;
}, {});
return returnObj;
},
/**
* Get item
* @param {DRP_MethodParams} params
*/
async (params) => {
let returnObj = {};
let targetType = params.__pathList.shift();
if (!levelDefinitions[targetType]) {
throw new DRP_CmdError(`Invalid path`, DRP_ErrorCode.NOTFOUND, "PathCmd");
}
returnObj = await levelDefinitions[targetType];
return returnObj;
},
// Permission set
null
)
return returnObj;
},
// Permission set
null
)
}
};
}
/**
* @param {DRP_MethodParams} params Remaining path
* @param {Boolean} baseObj Flag to return list of children
* @param {object} callingEndpoint Endpoint making request
* @returns {object} oReturnObject Return object
*/
async PathCmd(params, oCurrentObject, callingEndpoint) {
/*
* REST Calls
* params.__method = ('GetItem'|'GetChildItems'|'SetItem')
*
* RPC Calls
* params.__method = ('cat'|'ls'|'exec')
*
* DRP_VirtualFunction accepts methods 'execute' and 'SetItem'
*
* Using a subset of PowerShell NavigationCmdle