UNPKG

@hashgraph/hedera-local

Version:

Developer tooling for running Local Hedera Network (Consensus + Mirror Nodes).

430 lines 20.5 kB
"use strict"; // SPDX-License-Identifier: Apache-2.0 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.DockerService = void 0; const dockerode_1 = __importDefault(require("dockerode")); const shelljs_1 = __importDefault(require("shelljs")); const semver_1 = __importDefault(require("semver")); const fs_1 = __importDefault(require("fs")); const constants_1 = require("../constants"); const LoggerService_1 = require("./LoggerService"); const ServiceLocator_1 = require("./ServiceLocator"); const detect_port_1 = __importDefault(require("detect-port")); const dotenv = __importStar(require("dotenv")); const path_1 = __importDefault(require("path")); const SafeDockerNetworkRemover_1 = require("../utils/SafeDockerNetworkRemover"); const js_yaml_1 = __importDefault(require("js-yaml")); dotenv.config(); /** * DockerService is a service class that handles Docker-related operations. * It implements the IService interface. * It uses the 'dockerode' library to interact with Docker, 'shelljs' to execute shell commands, and 'semver' to compare semantic versioning numbers. */ class DockerService { /** * Constructs a new instance of the DockerService. * Initializes the logger and Docker socket path. */ constructor() { this.serviceName = DockerService.name; this.logger = ServiceLocator_1.ServiceLocator.Current.get(LoggerService_1.LoggerService.name); this.logger.trace(`${constants_1.CHECK_SUCCESS} Docker Service Initialized!`, this.serviceName); const defaultSocketPath = constants_1.IS_WINDOWS ? '//./pipe/docker_engine' : '/var/run/docker.sock'; this.dockerSocket = process.env.DOCKER_SOCKET || defaultSocketPath; } /** * Returns the Docker socket path. * * @public * @returns {string} - The Docker socket path. */ getDockerSocket() { return this.dockerSocket; } /** * Returns the null output path depending on the operating system. * * @public * @returns {string} - The null output path. */ getNullOutput() { if (constants_1.IS_WINDOWS) return 'null'; return '/dev/null'; } /** * Checks if Docker is running. * * @public * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether Docker is running. */ checkDocker() { return __awaiter(this, void 0, void 0, function* () { let isRunning = false; const docker = new dockerode_1.default({ socketPath: this.dockerSocket }); yield docker .info() .then(() => { this.logger.trace(`${constants_1.CHECK_SUCCESS} Docker is running.`, this.serviceName); isRunning = true; }) .catch(() => { this.logger.error(`${constants_1.CHECK_FAIL} Docker is not running.`, this.serviceName); isRunning = false; }); return isRunning; }); } /** * Checks if the provided ports are in use. * If necessary ports are in use, it terminates the process. * * @param {number[]} portsToCheck - The ports to check. * @public * @returns {Promise<void>} - A promise that resolves when the ports are checked. * @throws If an error occurs during the port check. */ isPortInUse(portsToCheck) { return __awaiter(this, void 0, void 0, function* () { const promises = portsToCheck.map((port) => (0, detect_port_1.default)(port) .then((available) => available !== port) .catch((error) => { // Handle the error throw error; })); const resolvedPromises = yield Promise.all(promises); resolvedPromises.forEach((result, index) => { const port = portsToCheck[index]; if (result && constants_1.OPTIONAL_PORTS.includes(port)) { this.logger.warn(`Port ${port} is in use.`, this.serviceName); } else if (result && constants_1.NECESSARY_PORTS.includes(port)) { this.logger.error(`${constants_1.CHECK_FAIL} Port ${port} is in use.`, this.serviceName); } }); const resolvedPromisesNecessaryPortsOnly = resolvedPromises.slice(0, constants_1.NECESSARY_PORTS.length); if (!(resolvedPromisesNecessaryPortsOnly.every(value => value === false))) { this.logger.error(`${constants_1.CHECK_FAIL} Node cannot start properly because necessary ports are in use!`, this.serviceName); process.exit(1); } }); } /** * Checks if the installed Docker Compose version is correct (greater than 2.12.2). * * @public * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether the Docker Compose version is correct. */ isCorrectDockerComposeVersion() { return __awaiter(this, void 0, void 0, function* () { this.logger.info(`${constants_1.LOADING} Checking docker compose version...`, this.serviceName); // We are executing both commands because in Linux we may have docker-compose v2, so we need to check both const resultFirstCommand = yield shelljs_1.default.exec('docker compose version --short', { silent: true }); const resultSecondCommand = yield shelljs_1.default.exec('docker-compose version --short', { silent: true }); // Exit code is 127 when no docker installation is found if (resultFirstCommand.code === 127 && resultSecondCommand.code === 127) { this.logger.error(`Please install docker compose V2.`, this.serviceName); } else if (resultFirstCommand.code === 127 && resultSecondCommand.code === 0) { this.logger.error(`Looks like you have docker-compose V1, but you need docker compose V2!`, this.serviceName); } else { const version = resultFirstCommand.stdout ? resultFirstCommand.stdout : resultSecondCommand.stdout; if (semver_1.default.gt(version, '2.12.2')) { // Docker version is OK return true; } this.logger.error('You are using docker compose version prior to 2.12.2, please upgrade', this.serviceName); } return false; }); } checkDockerResources(isMultiNodeMode) { return __awaiter(this, void 0, void 0, function* () { this.logger.info(`${constants_1.LOADING} Checking docker resources...`, this.serviceName); const ncpu = yield shelljs_1.default.exec("docker info --format='{{.NCPU}}'", { silent: true }); const memTotal = yield shelljs_1.default.exec("docker info --format='{{.MemTotal}}'", { silent: true }); const dockerMemory = Math.round(parseInt(memTotal) / Math.pow(1024, 3)); const dockerCPUs = parseInt(ncpu); return this.checkMemoryResources(dockerMemory, isMultiNodeMode) && this.checkCPUResources(dockerCPUs); }); } checkDockerImages() { const dockerComposeYml = js_yaml_1.default.load(shelljs_1.default.exec("docker compose config", { silent: true }).stdout); const dockerComposeImages = Object.values(dockerComposeYml.services).map((s) => { var _a; if (s.image) { const parsed = s.image.split(":"); return `${parsed[0]}:${(_a = parsed[1]) !== null && _a !== void 0 ? _a : "latest"}`; } }); const dockerComposeImagesUnique = [...new Set(dockerComposeImages.sort())]; const dockerImagesString = shelljs_1.default.exec("docker images", { silent: true }).stdout.split(/\r?\n/).slice(1, -1); const dockerImages = dockerImagesString.map(line => { const parsed = line.replace(/\s\s+/g, " ").split(" "); return `${parsed[0]}:${parsed[1]}`; }); const dockerImagesUnique = [...new Set(dockerImages.sort())]; if (!dockerImages.length || dockerComposeImagesUnique.toString() != dockerImagesUnique.toString()) { this.logger.info(constants_1.DOCKER_PULLING_IMAGES_MESSAGE, this.serviceName); } } checkMemoryResources(dockerMemory, isMultiNodeMode) { if ((dockerMemory >= constants_1.MIN_MEMORY_SINGLE_MODE && dockerMemory < constants_1.RECOMMENDED_MEMORY_SINGLE_MODE && !isMultiNodeMode) || (dockerMemory < constants_1.MIN_MEMORY_SINGLE_MODE && !isMultiNodeMode) || (dockerMemory < constants_1.MIN_MEMORY_MULTI_MODE && isMultiNodeMode)) { if (dockerMemory < constants_1.MIN_MEMORY_SINGLE_MODE) { this.handleMemoryError(dockerMemory, isMultiNodeMode); } else { this.logger.warn(`Your docker memory resources are ${dockerMemory.toFixed(2)}GB, which may cause unstable behaviour. Set to at least ${isMultiNodeMode ? constants_1.MIN_MEMORY_MULTI_MODE : constants_1.RECOMMENDED_MEMORY_SINGLE_MODE}GB`, this.serviceName); return true; } return false; } return true; } checkCPUResources(dockerCPUs) { if (dockerCPUs >= constants_1.MIN_CPUS && dockerCPUs < constants_1.RECOMMENDED_CPUS && !process.env.CI) { this.logger.warn(`Your docker CPU resources are set to ${dockerCPUs}, which may cause unstable behaviour. Set to at least ${constants_1.RECOMMENDED_CPUS} CPUs`, this.serviceName); return true; } else if (dockerCPUs < constants_1.MIN_CPUS && !process.env.CI) { this.logger.error(`Your docker CPU resources are set to ${dockerCPUs}. This is not enough, set to at least ${constants_1.RECOMMENDED_CPUS} CPUs`, this.serviceName); return false; } return true; } handleMemoryError(dockerMemory, isMultiNodeMode) { const recommendedMemory = isMultiNodeMode ? constants_1.MIN_MEMORY_MULTI_MODE : constants_1.MIN_MEMORY_SINGLE_MODE; this.logger.error(`Your docker memory resources are set to ${dockerMemory.toFixed(2)}GB. This is not enough, set to at least ${recommendedMemory}GB`, this.serviceName); } logShellOutput(shellExec) { [shellExec.stdout, shellExec.stderr].forEach((output) => { output.split("\n").map((line) => { if (line.indexOf(constants_1.SHARED_PATHS_ERROR) > -1 || line.indexOf(constants_1.MOUNT_ERROR) > -1) { this.logger.error(`Hedera local node start up TERMINATED due to docker's misconfiguration`); this.logger.error(constants_1.SHARED_PATHS_ERROR); this.logger.error(`See https://docs.docker.com/desktop/settings/mac/#file-sharing for more info.`); this.logger.error(`-- Make sure you have '/Users/<your_user>/Library/' under File Sharing Docker's Setting.`); this.logger.error(`-- If you're using hedera-local as npm package - running 'npm root -g' should output the path you have to add under File Sharing Docker's Setting.`); this.logger.error(`-- If you're using hedera-local as cloned repo - running 'pwd' in the project's root should output the path you have to add under File Sharing Docker's Setting.`); process.exit(); } if (line === "") return; this.logger.debug(line, this.serviceName); }); }); } executeExternal(command_1) { return __awaiter(this, arguments, void 0, function* (command, options = {}) { this.logger.trace(`🚀 Executing command: ${command}`, this.serviceName); const shellExec = shelljs_1.default.exec(command, options); this.logShellOutput(shellExec); return shellExec; }); } /** * Returns a Docker container object for the given container label. * * @param {string} containerLabel - The label of the container. * @returns {Promise<Dockerode.Container>} - A promise that resolves to a Docker container object. * @public */ getContainer(containerLabel) { return __awaiter(this, void 0, void 0, function* () { const containerId = yield this.getContainerId(containerLabel); const docker = new dockerode_1.default({ socketPath: this.getDockerSocket(), }); return docker.getContainer(containerId); }); } /** * Returns the ID of the Docker container with the given name. * * @param {string} name - The name of the container. * @returns {Promise<string>} - A promise that resolves to the ID of the Docker container. * @public */ getContainerId(name) { return __awaiter(this, void 0, void 0, function* () { const docker = new dockerode_1.default({ socketPath: this.dockerSocket }); const opts = { limit: 1, filters: { name: [`${name}`] } }; return new Promise((resolve, reject) => { docker.listContainers(opts, (err, containers) => { if (err) { reject(err); } else { resolve(containers[0].Id); } }); }); }); } /** * Returns the version of the Docker container with the given name. * * @param {string} name - The name of the container. * @returns {Promise<string>} - A promise that resolves to the version of the Docker container. * @public * @async */ getContainerVersion(name) { return __awaiter(this, void 0, void 0, function* () { const docker = new dockerode_1.default({ socketPath: this.dockerSocket }); const opts = { limit: 1, filters: { name: [`${name}`] } }; return new Promise((resolve, reject) => { docker.listContainers(opts, (err, containers) => { if (err) { reject(err); } else { try { resolve(containers[0].Image.split(':')[1]); } catch (e) { resolve(constants_1.UNKNOWN_VERSION); } } }); }); }); } /** * Executes the docker compose up command. * * @private * @returns {Promise<shell.ShellString>} A promise that resolves with the output of the command. */ dockerComposeUp(cliOptions) { return __awaiter(this, void 0, void 0, function* () { // TODO: Add multi node option const composeFiles = ['docker-compose.yml']; const { fullMode } = cliOptions; const { userCompose } = cliOptions; const { userComposeDir } = cliOptions; const { multiNode } = cliOptions; const { blockNode } = cliOptions; if (!fullMode) { composeFiles.push('docker-compose.evm.yml'); } if (!blockNode) { composeFiles.push('docker-compose.block-node.yml'); } if (multiNode) { composeFiles.push('docker-compose.multinode.yml'); if (blockNode) { composeFiles.push('docker-compose.multinode.blocknode.yml'); } } if (!fullMode && multiNode) { composeFiles.push('docker-compose.multinode.evm.yml'); } if (userCompose) { composeFiles.push(...this.getUserComposeFiles(userComposeDir)); } return this.executeExternal(`docker compose -f ${composeFiles.join(' -f ')} up -d`, { silent: true }); }); } /** * Retrieves an array of user compose files from the specified directory. * * @private * @param {string} userComposeDir - The directory path where the user compose files are located. Defaults to './overrides/'. * @returns {Array<string>} An array of user compose file paths. */ getUserComposeFiles(userComposeDir = './overrides/') { let dirPath = path_1.default.normalize(userComposeDir); if (!dirPath.endsWith(path_1.default.sep)) { dirPath += path_1.default.sep; } if (fs_1.default.existsSync(dirPath)) { const files = fs_1.default .readdirSync(dirPath) .filter((file) => path_1.default.extname(file).toLowerCase() === '.yml') .sort() .map((file) => dirPath.concat(file)); return files; } return []; } /** * Tries to recover the state by performing Docker recovery steps. * Stops the docker containers, cleans volumes and temp files, and tries to startup again. * @returns {Promise<void>} A promise that resolves when the recovery steps have completed. */ tryDockerRecovery(stateName) { return __awaiter(this, void 0, void 0, function* () { const nullOutput = this.getNullOutput(); this.logger.trace('Stopping the docker containers...', stateName); this.executeExternal(`docker compose kill --remove-orphans`, { silent: true }); this.executeExternal(`docker compose down -v --remove-orphans`, { silent: true }); this.logger.trace('Cleaning the volumes and temp files...', stateName); shelljs_1.default.exec(`rm -rf network-logs/* >${nullOutput} 2>&1`); SafeDockerNetworkRemover_1.SafeDockerNetworkRemover.removeAll(); this.logger.info(`${constants_1.LOADING} Trying to startup again...`, stateName); }); } } exports.DockerService = DockerService; //# sourceMappingURL=DockerService.js.map