@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
475 lines (474 loc) • 20.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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.PodmanClient = void 0;
exports.initClient = initClient;
const utils_1 = require("@zombienet/utils");
const execa_1 = __importDefault(require("execa"));
const fs_extra_1 = require("fs-extra");
const path_1 = __importStar(require("path"));
const yaml_1 = __importDefault(require("yaml"));
const constants_1 = require("../../constants");
const client_1 = require("../client");
const dynResourceDefinition_1 = require("./dynResourceDefinition");
const fs = require("fs").promises;
const debug = require("debug")("zombie::podman::client");
function initClient(configPath, namespace, tmpDir) {
const client = new PodmanClient(configPath, namespace, tmpDir);
(0, client_1.setClient)(client);
return client;
}
class PodmanClient extends client_1.Client {
constructor(configPath, namespace, tmpDir) {
super(configPath, namespace, tmpDir, "podman", "podman");
this.podMonitorAvailable = false;
this.configPath = configPath;
this.namespace = namespace;
this.debug = true;
this.timeout = 30; // secs
this.tmpDir = tmpDir;
this.localMagicFilepath = `${tmpDir}/finished.txt`;
this.remoteDir = constants_1.DEFAULT_REMOTE_DIR;
this.dataDir = constants_1.DEFAULT_DATA_DIR;
this.isTearingDown = false;
}
validateAccess() {
return __awaiter(this, void 0, void 0, function* () {
try {
const result = yield this.runCommand(["--help"], { scoped: false });
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);
// Podman don't have the namespace concept yet but we use a isolated network
const args = ["network", "create", this.namespace];
yield this.runCommand(args, { scoped: false });
return;
});
}
// start a grafana and prometheus
staticSetup(settings) {
return __awaiter(this, void 0, void 0, function* () {
const prometheusSpec = yield (0, dynResourceDefinition_1.genPrometheusDef)(this.namespace);
const promPort = prometheusSpec.spec.containers[0].ports[0].hostPort;
yield this.createResource(prometheusSpec, false, true);
const listeningIp = settings.local_ip || constants_1.LOCALHOST;
console.log(`\n\t Monitor: ${utils_1.decorators.green(prometheusSpec.metadata.name)} - url: http://${listeningIp}:${promPort}`);
const tempoSpec = yield (0, dynResourceDefinition_1.genTempoDef)(this.namespace);
yield this.createResource(tempoSpec, false, false);
const tempoPort = tempoSpec.spec.containers[0].ports[1].hostPort;
console.log(`\n\t Monitor: ${utils_1.decorators.green(tempoSpec.metadata.name)} - url: http://${listeningIp}:${tempoPort}`);
const prometheusIp = yield this.getNodeIP("prometheus");
const tempoIp = yield this.getNodeIP("tempo");
const grafanaSpec = yield (0, dynResourceDefinition_1.genGrafanaDef)(this.namespace, prometheusIp.toString(), tempoIp.toString());
yield this.createResource(grafanaSpec, false, false);
const grafanaPort = grafanaSpec.spec.containers[0].ports[0].hostPort;
console.log(`\n\t Monitor: ${utils_1.decorators.green(grafanaSpec.metadata.name)} - url: http://${listeningIp}:${grafanaPort}`);
});
}
createStaticResource(filename, replacements) {
return __awaiter(this, void 0, void 0, function* () {
const filePath = (0, path_1.resolve)(__dirname, `../../../static-configs/${filename}`);
const fileContent = yield fs.readFile(filePath);
let resourceDef = fileContent
.toString("utf-8")
.replace(new RegExp("{{namespace}}", "g"), this.namespace);
if (replacements) {
for (const replacementKey of Object.keys(replacements)) {
resourceDef = resourceDef.replace(new RegExp(`{{${replacementKey}}}`, "g"), replacements[replacementKey]);
}
}
const doc = new yaml_1.default.Document(JSON.parse(resourceDef));
const docInYaml = doc.toString();
const localFilePath = `${this.tmpDir}/${filename}`;
yield fs.writeFile(localFilePath, docInYaml);
yield this.runCommand([
"play",
"kube",
"--network",
this.namespace,
localFilePath,
]);
});
}
createPodMonitor() {
return __awaiter(this, void 0, void 0, function* () {
// NOOP, podman 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* () {
this.isTearingDown = true;
// get pod names
let args = [
"pod",
"ps",
"-f",
`label=zombie-ns=${this.namespace}`,
"--format",
"{{.Name}}",
];
let result = yield this.runCommand(args, { scoped: false });
// now remove the pods
args = ["pod", "rm", "-f", "-i", ...result.stdout.split("\n")];
result = yield this.runCommand(args, { scoped: false });
// now remove the pnetwork
args = ["network", "rm", "-f", this.namespace];
result = yield this.runCommand(args, { scoped: false });
});
}
addNodeToPrometheus(podName) {
return __awaiter(this, void 0, void 0, function* () {
const podIp = yield this.getNodeIP(podName);
const content = `[{"labels": {"pod": "${podName}"}, "targets": ["${podIp}:${constants_1.PROMETHEUS_PORT}"]}]`;
yield fs.writeFile(`${this.tmpDir}/prometheus/data/sd_config_${podName}.json`, content);
});
}
getNodeLogs(podName_1) {
return __awaiter(this, arguments, void 0, function* (podName, since = undefined) {
const args = ["logs"];
if (since && since > 0)
args.push(...["--since", `${since}s`]);
args.push(`${podName}_pod-${podName}`);
const result = yield this.runCommand(args, { scoped: false });
return result.stdout;
});
}
dumpLogs(path, podName) {
return __awaiter(this, void 0, void 0, function* () {
const dstFileName = `${path}/logs/${podName}.log`;
const logs = yield this.getNodeLogs(podName);
yield fs.writeFile(dstFileName, logs);
});
}
upsertCronJob() {
throw new Error("Method not implemented.");
}
startPortForwarding(port, identifier) {
return __awaiter(this, void 0, void 0, function* () {
const podName = identifier.includes("/")
? identifier.split("/")[1]
: identifier;
const hostPort = yield this.getPortMapping(port, podName);
return hostPort;
});
}
getPortMapping(port, podName) {
return __awaiter(this, void 0, void 0, function* () {
const args = ["inspect", `${podName}_pod-${podName}`, "--format", "json"];
const result = yield this.runCommand(args, { scoped: false });
const resultJson = JSON.parse(result.stdout);
const hostPort = resultJson[0].NetworkSettings.Ports[`${port}/tcp`][0].HostPort;
return hostPort;
});
}
getNodeIP(podName) {
return __awaiter(this, void 0, void 0, function* () {
const args = ["inspect", `${podName}_pod-${podName}`, "--format", "json"];
const result = yield this.runCommand(args, { scoped: false });
const resultJson = JSON.parse(result.stdout);
const podIp = resultJson[0].NetworkSettings.Networks[this.namespace].IPAddress;
return podIp;
});
}
getNodeInfo(podName_1, port_1) {
return __awaiter(this, arguments, void 0, function* (podName, port, externalView = false) {
let hostIp, hostPort;
if (externalView) {
hostPort = yield (port
? this.getPortMapping(port, podName)
: this.getPortMapping(constants_1.P2P_PORT, podName));
hostIp = yield (0, utils_1.getHostIp)();
}
else {
hostIp = yield this.getNodeIP(podName);
hostPort = port ? port : constants_1.P2P_PORT;
}
return [hostIp, hostPort];
});
}
runCommand(args, opts) {
return __awaiter(this, void 0, void 0, function* () {
try {
const augmentedCmd = [];
if (opts === null || opts === void 0 ? void 0 : opts.scoped)
augmentedCmd.push("--network", this.namespace);
const finalArgs = [...augmentedCmd, ...args];
const result = yield (0, execa_1.default)(this.command, finalArgs);
// podman use stderr for logs
const stdout = result.stdout !== ""
? result.stdout
: result.stderr !== ""
? result.stderr
: "";
return {
exitCode: result.exitCode,
stdout,
};
}
catch (error) {
// We prevent previous commands ran to throw error when we are tearing down the network.
if (!this.isTearingDown) {
console.log(`\n ${utils_1.decorators.red("Error: ")} \t ${utils_1.decorators.bright(error)}\n`);
throw error;
}
return { exitCode: 0, stdout: "" };
}
});
}
runScript(podName_1, scriptPath_1) {
return __awaiter(this, arguments, void 0, function* (podName, scriptPath, args = []) {
try {
const scriptFileName = path_1.default.basename(scriptPath);
const scriptPathInPod = `/tmp/${scriptFileName}`;
const identifier = `${podName}_pod-${podName}`;
// upload the script
yield this.runCommand([
"cp",
scriptPath,
`${identifier}:${scriptPathInPod}`,
]);
// set as executable
const baseArgs = ["exec", identifier];
yield this.runCommand([...baseArgs, "/bin/chmod", "+x", scriptPathInPod]);
// exec
const result = yield this.runCommand([
...baseArgs,
"bash",
"-c",
scriptPathInPod,
...args,
]);
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;
let logTable = new utils_1.CreateLogTable({
colWidths: [25, 100],
});
const logs = [
[utils_1.decorators.cyan("Pod"), utils_1.decorators.green(podDef.metadata.name)],
[utils_1.decorators.cyan("Status"), utils_1.decorators.green("Launching")],
[
utils_1.decorators.cyan("Command"),
utils_1.decorators.white(podDef.spec.containers[0].command.join(" ")),
],
];
if (dbSnapshot) {
logs.push([utils_1.decorators.cyan("DB Snapshot"), utils_1.decorators.green(dbSnapshot)]);
}
logTable.pushToPrint(logs);
// initialize keystore
const dataPath = podDef.spec.volumes.find((vol) => vol.name === "tmp-data");
debug("dataPath", dataPath);
if (dbSnapshot) {
// we need to get the snapshot from a public access
// and extract to /data
yield (0, utils_1.makeDir)(`${dataPath.hostPath.path}/chains`, true);
yield (0, utils_1.downloadFile)(dbSnapshot, `${dataPath.hostPath.path}/db.tgz`);
yield (0, execa_1.default)("bash", [
"-c",
`cd ${dataPath.hostPath.path}/.. && tar -xzvf data/db.tgz`,
]);
}
if (keystore && chainSpecId) {
const keystoreRemoteDir = `${dataPath.hostPath.path}/chains/${chainSpecId}/keystore`;
yield (0, utils_1.makeDir)(keystoreRemoteDir, true);
const keystoreIsEmpty = (yield fs.readdir(keystoreRemoteDir).length) === 0;
if (!keystoreIsEmpty)
yield this.runCommand(["unshare", "chmod", "-R", "o+w", keystoreRemoteDir], { scoped: false });
debug("keystoreRemoteDir", keystoreRemoteDir);
// inject keys
yield (0, fs_extra_1.copy)(keystore, keystoreRemoteDir);
debug("keys injected");
}
const cfgDirIsEmpty = (yield fs.readdir(`${this.tmpDir}/${name}/cfg`).length) === 0;
if (!cfgDirIsEmpty && filesToCopy.length) {
yield this.runCommand(["unshare", "chmod", "-R", "o+w", `${this.tmpDir}/${name}/cfg`], { scoped: false });
}
// copy files to volumes
for (const fileMap of filesToCopy) {
const { localFilePath, remoteFilePath } = fileMap;
debug(`copyFile ${localFilePath} to ${this.tmpDir}/${name}${remoteFilePath}`);
yield fs.cp(localFilePath, `${this.tmpDir}/${name}${remoteFilePath}`);
debug("copied!");
}
yield this.createResource(podDef, false, false);
yield this.wait_pod_ready(name);
yield this.addNodeToPrometheus(name);
logTable = new utils_1.CreateLogTable({
colWidths: [20, 100],
});
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 ${this.tmpDir}/${identifier}${podFilePath} ${localFilePath}`);
yield fs.copyFile(`${this.tmpDir}/${identifier}${podFilePath}`, localFilePath);
});
}
putLocalMagicFile() {
return __awaiter(this, void 0, void 0, function* () {
// NOOP
return;
});
}
createResource(resourseDef, scoped, waitReady) {
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.writeFile(localFilePath, docInYaml);
yield this.runCommand(["play", "kube", "--network", this.namespace, localFilePath], { scoped: false });
if (waitReady)
yield this.wait_pod_ready(name);
});
}
wait_pod_ready(podName_1) {
return __awaiter(this, arguments, void 0, function* (podName, allowDegraded = true) {
// loop until ready
let t = this.timeout;
const args = ["pod", "ps", "-f", `name=${podName}`, "--format", "json"];
do {
const result = yield this.runCommand(args, { scoped: false });
const resultJson = JSON.parse(result.stdout);
if (resultJson[0].Status === "Running")
return;
if (allowDegraded && resultJson[0].Status === "Degraded")
return;
yield new Promise((resolve) => setTimeout(resolve, 3000));
t -= 3;
} while (t > 0);
throw new Error(`Timeout(${this.timeout}) for pod : ${podName}`);
});
}
isPodMonitorAvailable() {
return __awaiter(this, void 0, void 0, function* () {
// NOOP
return false;
});
}
getPauseArgs(name) {
return [
"exec",
`${name}_pod-${name}`,
"bash",
"-c",
"echo pause > /tmp/zombiepipe",
];
}
getResumeArgs(name) {
return [
"exec",
`${name}_pod-${name}`,
"bash",
"-c",
"echo resume > /tmp/zombiepipe",
];
}
restartNode(name, timeout) {
return __awaiter(this, void 0, void 0, function* () {
const args = ["exec", `${name}_pod-${name}`, "bash", "-c"];
const cmd = timeout
? `echo restart ${timeout} > /tmp/zombiepipe`
: `echo restart > /tmp/zombiepipe`;
args.push(cmd);
const result = yield this.runCommand(args, { scoped: false });
return result.exitCode === 0;
});
}
spawnIntrospector(wsUri) {
return __awaiter(this, void 0, void 0, function* () {
const spec = yield (0, dynResourceDefinition_1.getIntrospectorDef)(this.namespace, wsUri);
yield this.createResource(spec, false, true);
});
}
getLogsCommand(name) {
return `podman logs -f ${name}_pod-${name}`;
}
// NOOP
// eslint-disable-next-line
injectChaos(_chaosSpecs) {
return __awaiter(this, void 0, void 0, function* () { });
}
}
exports.PodmanClient = PodmanClient;