@zombienet/orchestrator
Version:
ZombieNet aim to be a testing framework for substrate based blockchains, providing a simple cli tool that allow users to spawn and test ephemeral Substrate based networks
334 lines (333 loc) • 13.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Network = exports.Scope = void 0;
exports.rebuildNetwork = rebuildNetwork;
const utils_1 = require("@zombienet/utils");
const fs_1 = __importDefault(require("fs"));
const constants_1 = require("./constants");
const networkNode_1 = require("./networkNode");
const debug = require("debug")("zombie::network");
var Scope;
(function (Scope) {
Scope[Scope["RELAY"] = 0] = "RELAY";
Scope[Scope["PARA"] = 1] = "PARA";
Scope[Scope["COMPANION"] = 2] = "COMPANION";
})(Scope || (exports.Scope = Scope = {}));
function rebuildNetwork(client, runningNetworkSpec) {
const { namespace, tmpDir, companions, launched, backchannel, chainSpecFullPath, nodesByName, tracing_collator_url, } = runningNetworkSpec;
const network = new Network(client, namespace, tmpDir);
Object.assign(network, {
companions,
launched,
backchannel,
chainSpecFullPath,
tracing_collator_url,
});
for (const nodeName of Object.keys(nodesByName)) {
const node = nodesByName[nodeName];
const networkNode = new networkNode_1.NetworkNode(node.name, node.wsUri, node.prometheusUri, node.userDefinedTypes);
if (node.parachainId) {
if (!network.paras[node.parachainId])
network.addPara(node.parachainId, node.parachainSpecPath);
networkNode.parachainId = node.parachainId;
}
networkNode.group = node.group;
network.addNode(networkNode, node.parachainId ? Scope.PARA : Scope.RELAY);
}
// ensure keep running by mark that was already running
network.wasRunning = true;
return network;
}
class Network {
constructor(client, namespace, tmpDir, startTime = new Date().getTime()) {
this.relay = [];
this.paras = {};
this.groups = {};
this.companions = [];
this.nodesByName = {};
this.launched = false;
this.wasRunning = false;
this.backchannelUri = "";
this.client = client;
this.namespace = namespace;
this.tmpDir = tmpDir;
this.networkStartTime = startTime;
}
addPara(parachainId, chainSpecPath, wasmPath, statePath) {
if (!this.paras[parachainId]) {
this.paras[parachainId] = {
nodes: [],
chainSpecPath,
wasmPath,
statePath,
};
}
}
addNode(node, scope) {
if (scope === Scope.RELAY)
this.relay.push(node);
else if (scope == Scope.COMPANION)
this.companions.push(node);
else {
if (!node.parachainId || !this.paras[node.parachainId])
throw new Error("Invalid network node configuration, collator must set the parachainId");
this.paras[node.parachainId].nodes.push(node);
}
this.nodesByName[node.name] = node;
if (node.group) {
if (!this.groups[node.group])
this.groups[node.group] = [];
this.groups[node.group].push(node);
}
}
stop() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
// Cleanup all api instances
for (const node of Object.values(this.nodesByName))
(_a = node.apiInstance) === null || _a === void 0 ? void 0 : _a.disconnect();
yield this.client.destroyNamespace();
});
}
dumpLogs() {
return __awaiter(this, arguments, void 0, function* (showLogPath = true) {
const logsPath = this.tmpDir + "/logs";
// create dump directory in local temp
try {
yield fs_1.default.promises.access(logsPath, fs_1.default.promises.constants.R_OK | fs_1.default.promises.constants.W_OK);
}
catch (_a) {
// create dir
yield fs_1.default.promises.mkdir(logsPath);
}
const paraNodes = Object.values(this.paras).reduce((memo, value) => memo.concat(value.nodes), []);
const dumpsNodes = this.relay.concat(paraNodes);
yield Promise.allSettled(dumpsNodes.map((node) => this.client.dumpLogs(this.tmpDir, node.name)));
if (showLogPath)
new utils_1.CreateLogTable({ colWidths: [20, 100] }).pushToPrint([
[utils_1.decorators.green("Node's logs:"), utils_1.decorators.magenta(logsPath)],
]);
return logsPath;
});
}
upsertCronJob() {
return __awaiter(this, arguments, void 0, function* (minutes = 10) {
yield this.client.upsertCronJob(minutes);
});
}
getBackchannelValue(key_1) {
return __awaiter(this, arguments, void 0, function* (key, timeout = constants_1.DEFAULT_INDIVIDUAL_TEST_TIMEOUT) {
let limitTimeout;
let expired = false;
let value;
try {
limitTimeout = setTimeout(() => {
expired = true;
}, timeout * 1000);
if (!this.backchannelUri) {
// create port-fw
const port = yield this.client.startPortForwarding(constants_1.BACKCHANNEL_PORT, constants_1.BACKCHANNEL_POD_NAME);
this.backchannelUri = constants_1.BACKCHANNEL_URI_PATTERN.replace("{{PORT}}", port.toString());
}
let done = false;
debug(`backchannel uri ${this.backchannelUri}`);
while (!done) {
if (expired)
throw new Error(`Timeout(${timeout}s)`);
const fetchResult = yield fetch(`${this.backchannelUri}/${key}`, {
signal: (0, utils_1.TimeoutAbortController)(2).signal,
});
const response = yield fetchResult.json();
const { status } = response;
debug(`status: ${status}`);
if (status === 404 || (status >= 200 && status < 300)) {
return status === 404 || (status >= 200 && status < 300);
}
if (response.status === 200) {
done = true;
value = response.data;
continue;
}
// wait 2 secs between checks
yield new Promise((resolve) => setTimeout(resolve, 2000));
}
return value;
}
catch (err) {
console.log(`\n ${utils_1.decorators.red("Error: ")} \t ${utils_1.decorators.bright(err)}\n`);
if (limitTimeout)
clearTimeout(limitTimeout);
throw err;
}
});
}
getNodeByName(nodeName) {
const node = this.nodesByName[nodeName];
if (!node)
throw new Error(`NODE: ${nodeName} not present`);
return node;
}
getNodes(nodeOrGroupName) {
//check if is a node
const node = this.nodesByName[nodeOrGroupName];
if (node)
return [node];
//check if is a group
const nodes = this.groups[nodeOrGroupName];
if (!nodes)
throw new Error(`Noode or Group: ${nodeOrGroupName} not present`);
return nodes;
}
node(nodeName) {
const node = this.nodesByName[nodeName];
if (!node)
throw new Error(`NODE: ${nodeName} not present`);
return node;
}
// Testing abstraction
nodeIsUp(nodeName) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const node = this.getNodeByName(nodeName);
yield ((_a = node.apiInstance) === null || _a === void 0 ? void 0 : _a.rpc.system.name());
return true;
}
catch (err) {
console.log(`\n ${utils_1.decorators.red("Error: ")} \t ${utils_1.decorators.bright(err)}\n`);
return false;
}
});
}
// show links for access and debug
showNetworkInfo(provider) {
const logTable = new utils_1.CreateLogTable({
head: [
{
colSpan: 2,
hAlign: "center",
content: utils_1.decorators.green("Network launched 🚀🚀"),
},
],
colWidths: [30, 100],
wordWrap: true,
});
logTable.pushTo([
["Namespace", this.namespace],
["Provider", this.client.providerName],
]);
for (const node of this.relay) {
this.showNodeInfo(node, provider, logTable);
}
for (const [paraId, parachain] of Object.entries(this.paras)) {
for (const node of parachain.nodes) {
this.showNodeInfo(node, provider, logTable);
}
logTable.pushTo([[utils_1.decorators.cyan("Parachain ID"), paraId]]);
if (parachain.chainSpecPath)
logTable.pushTo([
[utils_1.decorators.cyan("ChainSpec Path"), parachain.chainSpecPath],
]);
}
if (this.companions.length) {
logTable.pushTo([
[
{
colSpan: 2,
content: "Companions",
},
],
]);
for (const node of this.companions) {
this.showNodeInfo(node, provider, logTable);
}
}
// Add network-wide error logs link for kubernetes provider
if (this.client.providerName === "kubernetes" && this.networkStartTime) {
const inCI = process.env.RUN_IN_CONTAINER === "1";
if (inCI) {
const networkLokiUrl = (0, utils_1.getLokiUrlForNetworkErrors)(this.namespace, this.networkStartTime);
logTable.pushTo([
[
{
colSpan: 2,
hAlign: "center",
content: utils_1.decorators.cyan("🌐 All nodes logs (Grafana)"),
},
],
[
{
colSpan: 2,
content: utils_1.decorators.bright(networkLokiUrl),
},
],
]);
}
}
logTable.print();
}
showNodeInfo(node, provider, logTable) {
// Support native VSCode remote extension automatic port forwarding.
// VSCode doesn't parse the encoded URI and we have no reason to encode
// `localhost:port`.
const wsUri = ["native", "podman"].includes(provider)
? node.wsUri
: encodeURIComponent(node.wsUri);
let logCommand = "";
switch (this.client.providerName) {
case "podman":
logCommand = `podman logs -f ${node.name}_pod-${node.name}`;
break;
case "kubernetes":
logCommand = `kubectl logs -f ${node.name} -c ${node.name} -n ${this.client.namespace}`;
break;
case "native":
logCommand = `tail -f ${this.client.tmpDir}/${node.name}.log`;
break;
}
logTable.pushTo([
[{ colSpan: 2, hAlign: "center", content: "Node Information" }],
[utils_1.decorators.cyan("Name"), utils_1.decorators.green(node.name)],
[
utils_1.decorators.cyan("Direct Link (pjs)"),
`https://polkadot.js.org/apps/?rpc=${wsUri}#/explorer`,
],
[
utils_1.decorators.cyan("Direct Link (papi)"),
`https://dev.papi.how/explorer#networkId=custom&endpoint=${wsUri}`,
],
[utils_1.decorators.cyan("Prometheus Link"), node.prometheusUri],
[utils_1.decorators.cyan("Log Cmd"), logCommand],
]);
}
replaceWithNetworInfo(placeholder) {
return placeholder.replace(constants_1.TOKEN_PLACEHOLDER, (_substring, nodeNameOrNetwork, key) => {
if (nodeNameOrNetwork === "network") {
const key_to_use = key == "base_path" ? "tmpDir" : key;
return this[key_to_use];
}
else {
const node = this.getNodeByName(nodeNameOrNetwork);
return node[key];
}
});
}
cleanMetricsCache() {
for (const node of Object.values(this.nodesByName)) {
node.cleanMetricsCache();
}
}
}
exports.Network = Network;