@hashgraph/hedera-local
Version:
Developer tooling for running Local Hedera Network (Consensus + Mirror Nodes).
430 lines • 20.5 kB
JavaScript
;
// 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