UNPKG

@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

446 lines (445 loc) 18.7 kB
"use strict"; 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.NativeClient = void 0; exports.initClient = initClient; const utils_1 = require("@zombienet/utils"); const child_process_1 = require("child_process"); const execa_1 = __importDefault(require("execa")); const fs_extra_1 = require("fs-extra"); const path_1 = __importDefault(require("path")); const yaml_1 = __importDefault(require("yaml")); const constants_1 = require("../../constants"); const sharedTypes_1 = require("../../sharedTypes"); const client_1 = require("../client"); const fs = require("fs"); const debug = require("debug")("zombie::native::client"); function initClient(configPath, namespace, tmpDir) { const client = new NativeClient(configPath, namespace, tmpDir); (0, client_1.setClient)(client); return client; } class NativeClient extends client_1.Client { constructor(configPath, namespace, tmpDir) { super(configPath, namespace, tmpDir, "bash", "native"); this.podMonitorAvailable = false; this.configPath = configPath; this.namespace = namespace; this.debug = true; this.timeout = 60; // secs this.tmpDir = tmpDir; this.localMagicFilepath = `${tmpDir}/finished.txt`; this.processMap = {}; this.remoteDir = `${tmpDir}${constants_1.DEFAULT_REMOTE_DIR}`; this.dataDir = `${tmpDir}${constants_1.DEFAULT_DATA_DIR}`; this.dbSnapshotCache = {}; } validateAccess() { return __awaiter(this, void 0, void 0, function* () { try { const result = yield this.runCommand(["--help"]); return result.exitCode === 0; } catch (e) { return false; } }); } createNamespace() { return __awaiter(this, void 0, void 0, function* () { const namespaceDef = { apiVersion: "v1", kind: "Namespace", metadata: { name: this.namespace, }, }; (0, utils_1.writeLocalJsonFile)(this.tmpDir, "namespace", namespaceDef); // Native provider don't have the `namespace` isolation. // but we create the `remoteDir` to place files yield (0, utils_1.makeDir)(this.remoteDir, true); return; }); } // Podman ONLY support `pods` staticSetup() { return __awaiter(this, void 0, void 0, function* () { return; }); } createStaticResource() { return __awaiter(this, void 0, void 0, function* () { // NOOP, native don't have podmonitor. return; }); } createPodMonitor() { return __awaiter(this, void 0, void 0, function* () { // NOOP, native don't have podmonitor. return; }); } setupCleaner() { return __awaiter(this, void 0, void 0, function* () { // NOOP, podman don't have cronJobs return; }); } destroyNamespace() { return __awaiter(this, void 0, void 0, function* () { // get pod names const args = ["bash", "-c"]; const memo = []; const pids = Object.keys(this.processMap).reduce((memo, key) => { if (this.processMap[key] && this.processMap[key].pid) { const pid = this.processMap[key].pid; if (pid) memo.push(pid.toString()); } return memo; }, memo); const result = yield this.runCommand(["bash", "-c", `ps ax| awk '{print $1}'| grep -E '${pids.join("|")}'`], { allowFail: true }); if (result.exitCode === 0) { const pidsToKill = result.stdout.split("\n"); if (pidsToKill.length > 0) { args.push(`kill -9 ${pids.join(" ")}`); yield this.runCommand(args); } } }); } getNodeLogs(name) { return __awaiter(this, void 0, void 0, function* () { // For now in native let's just return all the logs const lines = yield fs.promises.readFile(`${this.tmpDir}/${name}.log`); return lines.toString(); }); } dumpLogs(path, podName) { return __awaiter(this, void 0, void 0, function* () { const dstFileName = `${path}/logs/${podName}.log`; yield fs.promises.copyFile(`${this.tmpDir}/${podName}.log`, dstFileName); }); } upsertCronJob() { throw new Error("Method not implemented."); } startPortForwarding(port, identifier) { return __awaiter(this, void 0, void 0, function* () { const podName = identifier.split("/")[1]; const hostPort = yield this.getPortMapping(port, podName); return hostPort; }); } getPortMapping(port, podName) { return __awaiter(this, void 0, void 0, function* () { return this.processMap[podName].portMapping[port]; }); } getNodeInfo(podName) { return __awaiter(this, void 0, void 0, function* () { const hostPort = yield this.getPortMapping(constants_1.P2P_PORT, podName); return [constants_1.LOCALHOST, hostPort]; }); } getNodeIP() { return __awaiter(this, void 0, void 0, function* () { return constants_1.LOCALHOST; }); } runCommand(args, opts) { return __awaiter(this, void 0, void 0, function* () { try { if (args[0] === "bash") args.splice(0, 1); debug(args); const result = yield (0, execa_1.default)(this.command, args); // podman use stderr for logs const stdout = result.stdout !== "" ? result.stdout : result.stderr !== "" ? result.stderr : ""; return { exitCode: result.exitCode, stdout, }; } catch (error) { debug(error); if (!(opts === null || opts === void 0 ? void 0 : opts.allowFail)) throw error; const { exitCode, stdout, message: errorMsg } = error; return { exitCode, stdout, errorMsg, }; } }); } runScript(identifier_1, scriptPath_1) { return __awaiter(this, arguments, void 0, function* (identifier, scriptPath, args = []) { try { const scriptFileName = path_1.default.basename(scriptPath); const scriptPathInPod = `${this.tmpDir}/${identifier}/${scriptFileName}`; // upload the script yield fs.promises.cp(scriptPath, scriptPathInPod); // set as executable yield (0, execa_1.default)(this.command, [ "-c", ["chmod", "+x", scriptPathInPod].join(" "), ]); // exec const result = yield (0, execa_1.default)(this.command, [ "-c", [ `cd ${this.tmpDir}/${identifier}`, "&&", scriptPathInPod, ...args, ].join(" "), ]); return { exitCode: result.exitCode, stdout: result.stdout, }; } catch (error) { debug(error); throw error; } }); } spawnFromDef(podDef_1) { return __awaiter(this, arguments, void 0, function* (podDef, filesToCopy = [], keystore, chainSpecId, dbSnapshot) { const name = podDef.metadata.name; debug(JSON.stringify(podDef, null, 4)); // keep this in the client. this.processMap[name] = { logs: `${this.tmpDir}/${name}.log`, portMapping: podDef.spec.ports.reduce((memo, item) => { memo[item.containerPort] = item.hostPort; return memo; }, {}), }; let logTable = new utils_1.CreateLogTable({ colWidths: [25, 100], }); const logs = [ [utils_1.decorators.cyan("Pod"), utils_1.decorators.green(name)], [utils_1.decorators.cyan("Status"), utils_1.decorators.green("Launching")], [ utils_1.decorators.cyan("Command"), utils_1.decorators.white(podDef.spec.command.join(" ")), ], ]; if (dbSnapshot) { logs.push([utils_1.decorators.cyan("DB Snapshot"), utils_1.decorators.green(dbSnapshot)]); } logTable.pushToPrint(logs); if (dbSnapshot) { // we need to get the snapshot from a public access // and extract to /data yield (0, utils_1.makeDir)(`${podDef.spec.dataPath}`, true); // check if we need to download the dbSnapshot or is already in the fs const dstPath = `${podDef.spec.dataPath}/db.tgz`; if (this.dbSnapshotCache[dbSnapshot]) { const srcPath = this.dbSnapshotCache[dbSnapshot]; debug(`copying snapshot from path: ${srcPath}`); yield this.runCommand(["-c", `cp ${srcPath} ${podDef.spec.dataPath}`]); } else { // download and cache debug(`downloading snapshot from url: ${dbSnapshot}`); yield (0, utils_1.downloadFile)(dbSnapshot, dstPath); this.dbSnapshotCache[dbSnapshot] = dstPath; } yield this.runCommand([ "-c", `cd ${podDef.spec.dataPath}/.. && tar -xzvf data/db.tgz`, ]); } if (keystore) { // initialize keystore const keystoreRemoteDir = `${podDef.spec.dataPath}/chains/${chainSpecId}/keystore`; yield (0, utils_1.makeDir)(keystoreRemoteDir, true); // inject keys yield (0, fs_extra_1.copy)(keystore, keystoreRemoteDir); } // copy files to volumes for (const fileMap of filesToCopy) { const { localFilePath, remoteFilePath } = fileMap; debug("localFilePath", localFilePath); debug("remoteFilePath", remoteFilePath); debug("remote dir", this.remoteDir); debug("data dir", this.dataDir); const resolvedRemoteFilePath = remoteFilePath.includes(this.remoteDir) ? `${podDef.spec.cfgPath}/${remoteFilePath.replace(this.remoteDir, "")}` : `${podDef.spec.dataPath}/${remoteFilePath.replace(this.dataDir, "")}`; yield fs.promises.copyFile(localFilePath, resolvedRemoteFilePath); } yield this.createResource(podDef); logTable = new utils_1.CreateLogTable({ colWidths: [40, 80], }); logTable.pushToPrint([ [utils_1.decorators.cyan("Pod"), utils_1.decorators.green(name)], [utils_1.decorators.cyan("Status"), utils_1.decorators.green("Ready")], ]); }); } copyFileFromPod(identifier, podFilePath, localFilePath) { return __awaiter(this, void 0, void 0, function* () { debug(`cp ${podFilePath} ${localFilePath}`); yield fs.promises.copyFile(podFilePath, localFilePath); }); } putLocalMagicFile() { return __awaiter(this, void 0, void 0, function* () { // NOOP return; }); } createResource(resourseDef) { return __awaiter(this, void 0, void 0, function* () { const name = resourseDef.metadata.name; const doc = new yaml_1.default.Document(resourseDef); const docInYaml = doc.toString(); const localFilePath = `${this.tmpDir}/${name}.yaml`; yield fs.promises.writeFile(localFilePath, docInYaml); if (resourseDef.metadata.labels["zombie-role"] === sharedTypes_1.ZombieRole.Temp) { yield this.runCommand(resourseDef.spec.command); } else { if (resourseDef.spec.command[0] === "bash") resourseDef.spec.command.splice(0, 1); debug(this.command); debug(resourseDef.spec.command); const log = fs.createWriteStream(this.processMap[name].logs); const nodeProcess = (0, child_process_1.spawn)(this.command, ["-c", ...resourseDef.spec.command], { env: Object.assign(Object.assign({}, process.env), resourseDef.spec.env) }); debug(nodeProcess.pid); nodeProcess.stdout.pipe(log); nodeProcess.stderr.pipe(log); this.processMap[name].pid = nodeProcess.pid; this.processMap[name].cmd = resourseDef.spec.command; yield this.wait_node_ready(name); } }); } wait_node_ready(nodeName) { return __awaiter(this, void 0, void 0, function* () { // check if the process is alive after 1 seconds yield (0, utils_1.sleep)(1000); const procNodeName = this.processMap[nodeName]; const { pid, logs } = procNodeName; let result = yield this.runCommand(["-c", `ps ${pid}`], { allowFail: true, }); if (result.exitCode > 0) { yield this.informProcessDie(pid, nodeName); throw new Error(); } // check log lines grows const lines_1 = yield this.runCommand(["-c", `wc -l ${logs}`]); yield (0, utils_1.sleep)(1000); const lines_2 = yield this.runCommand(["-c", `wc -l ${logs}`]); if (parseInt(lines_2.stdout.trim()) > parseInt(lines_1.stdout.trim())) return; yield (0, utils_1.sleep)(1000); const lines_3 = yield this.runCommand(["-c", `wc -l ${logs}`]); if (parseInt(lines_3.stdout.trim()) > parseInt(lines_1.stdout.trim())) return; // check if the process is still alive, IFF return node ready // Since could be that the LOG env is set to the minimum. result = yield this.runCommand(["-c", `ps ${pid}`], { allowFail: true, }); if (result.exitCode > 0) { yield this.informProcessDie(pid, nodeName); throw new Error(); } return; }); } informProcessDie(pid, nodeName) { return __awaiter(this, void 0, void 0, function* () { const lines = yield this.getNodeLogs(nodeName); const logTable = new utils_1.CreateLogTable({ colWidths: [20, 100], }); logTable.pushToPrint([ [utils_1.decorators.cyan("Pod"), utils_1.decorators.green(nodeName)], [utils_1.decorators.cyan("Status"), utils_1.decorators.reverse(utils_1.decorators.red("Error"))], [ utils_1.decorators.cyan("Message"), utils_1.decorators.white(`Process: ${pid}, for node: ${nodeName} dies.`), ], [utils_1.decorators.cyan("Output"), utils_1.decorators.white(lines)], ]); }); } isPodMonitorAvailable() { return __awaiter(this, void 0, void 0, function* () { // NOOP return false; }); } spawnIntrospector() { return __awaiter(this, void 0, void 0, function* () { // NOOP }); } getPauseArgs(name) { return ["-c", `kill -STOP ${this.processMap[name].pid.toString()}`]; } getResumeArgs(name) { return ["-c", `kill -CONT ${this.processMap[name].pid.toString()}`]; } restartNode(name, timeout) { return __awaiter(this, void 0, void 0, function* () { // kill const result = yield this.runCommand(["-c", `kill -9 ${this.processMap[name].pid.toString()}`], { allowFail: true }); if (result.exitCode !== 0) return false; // sleep if (timeout) yield (0, utils_1.sleep)(timeout * 1000); // start without override the log file. const log = fs.createWriteStream(this.processMap[name].logs, { flags: "a", }); console.log(["-c", ...this.processMap[name].cmd]); const nodeProcess = (0, child_process_1.spawn)(this.command, [ "-c", ...this.processMap[name].cmd, ]); debug(nodeProcess.pid); nodeProcess.stdout.pipe(log); nodeProcess.stderr.pipe(log); this.processMap[name].pid = nodeProcess.pid; yield this.wait_node_ready(name); return true; }); } getLogsCommand(name) { return `tail -f ${this.tmpDir}/${name}.log`; } // NOOP // eslint-disable-next-line injectChaos(_chaosSpecs) { return __awaiter(this, void 0, void 0, function* () { }); } } exports.NativeClient = NativeClient;