@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
353 lines • 14.2 kB
JavaScript
/**
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import util from 'util';
import * as semver from 'semver';
import { SoloError } from './errors.js';
import { Templates } from './templates.js';
import * as constants from './constants.js';
import { PrivateKey, ServiceEndpoint } from '@hashgraph/sdk';
import { fileURLToPath } from 'url';
import { NamespaceName } from './kube/resources/namespace/namespace_name.js';
export function getInternalIp(releaseVersion, namespaceName, nodeAlias) {
//? Explanation: for v0.59.x the internal IP address is set to 127.0.0.1 to avoid an ISS
let internalIp = '';
// for versions that satisfy 0.58.5+
// @ts-expect-error TS2353: Object literal may only specify known properties
if (semver.gte(releaseVersion, '0.58.5', { includePrerelease: true })) {
internalIp = '127.0.0.1';
}
// versions less than 0.58.5
else {
internalIp = Templates.renderFullyQualifiedNetworkPodName(namespaceName, nodeAlias);
}
return internalIp;
}
export async function getExternalAddress(consensusNode, k8, useLoadBalancer) {
if (useLoadBalancer) {
return resolveLoadBalancerAddress(consensusNode, k8);
}
return consensusNode.fullyQualifiedDomainName;
}
async function resolveLoadBalancerAddress(consensusNode, k8) {
const ns = NamespaceName.of(consensusNode.namespace);
const serviceList = await k8
.services()
.list(ns, [`solo.hedera.com/node-id=${consensusNode.nodeId},solo.hedera.com/type=network-node-svc`]);
if (serviceList && serviceList.length > 0) {
const svc = serviceList[0];
if (!svc.metadata.name.startsWith('network-node')) {
throw new SoloError(`Service found is not a network node service: ${svc.metadata.name}`);
}
if (svc.status?.loadBalancer?.ingress && svc.status.loadBalancer.ingress.length > 0) {
for (let i = 0; i < svc.status.loadBalancer.ingress.length; i++) {
const ingress = svc.status.loadBalancer.ingress[i];
if (ingress.hostname) {
return ingress.hostname;
}
else if (ingress.ip) {
return ingress.ip;
}
}
}
}
return consensusNode.fullyQualifiedDomainName;
}
export function sleep(duration) {
return new Promise(resolve => {
setTimeout(resolve, duration.toMillis());
});
}
export function parseNodeAliases(input) {
return splitFlagInput(input, ',');
}
export function splitFlagInput(input, separator = ',') {
if (!input) {
return [];
}
else if (typeof input !== 'string') {
throw new SoloError(`input [input='${input}'] is not a comma separated string`);
}
return input
.split(separator)
.map(s => s.trim())
.filter(Boolean);
}
/**
* @param arr - The array to be cloned
* @returns a new array with the same elements as the input array
*/
export function cloneArray(arr) {
return JSON.parse(JSON.stringify(arr));
}
export function getTmpDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'solo-'));
}
export function createBackupDir(destDir, prefix = 'backup', curDate = new Date()) {
const dateDir = util.format('%s%s%s_%s%s%s', curDate.getFullYear(), curDate.getMonth().toString().padStart(2, '0'), curDate.getDate().toString().padStart(2, '0'), curDate.getHours().toString().padStart(2, '0'), curDate.getMinutes().toString().padStart(2, '0'), curDate.getSeconds().toString().padStart(2, '0'));
const backupDir = path.join(destDir, prefix, dateDir);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
return backupDir;
}
export function makeBackup(fileMap = new Map(), removeOld = true) {
for (const entry of fileMap) {
const srcPath = entry[0];
const destPath = entry[1];
if (fs.existsSync(srcPath)) {
fs.cpSync(srcPath, destPath);
if (removeOld) {
fs.rmSync(srcPath);
}
}
}
}
export function backupOldTlsKeys(nodeAliases, keysDir, curDate = new Date(), dirPrefix = 'tls') {
const backupDir = createBackupDir(keysDir, `unused-${dirPrefix}`, curDate);
const fileMap = new Map();
for (const nodeAlias of nodeAliases) {
const srcPath = path.join(keysDir, Templates.renderTLSPemPrivateKeyFile(nodeAlias));
const destPath = path.join(backupDir, Templates.renderTLSPemPrivateKeyFile(nodeAlias));
fileMap.set(srcPath, destPath);
}
makeBackup(fileMap, true);
return backupDir;
}
export function backupOldPemKeys(nodeAliases, keysDir, curDate = new Date(), dirPrefix = 'gossip-pem') {
const backupDir = createBackupDir(keysDir, `unused-${dirPrefix}`, curDate);
const fileMap = new Map();
for (const nodeAlias of nodeAliases) {
const srcPath = path.join(keysDir, Templates.renderGossipPemPrivateKeyFile(nodeAlias));
const destPath = path.join(backupDir, Templates.renderGossipPemPrivateKeyFile(nodeAlias));
fileMap.set(srcPath, destPath);
}
makeBackup(fileMap, true);
return backupDir;
}
export function isNumeric(str) {
if (typeof str !== 'string')
return false; // we only process strings!
return (!isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
!isNaN(parseFloat(str))); // ...and ensure strings of whitespace fail
}
/**
* Validate a path provided by the user to prevent path traversal attacks
* @param input - the input provided by the user
* @returns a validated path
*/
export function validatePath(input) {
if (input.indexOf('\0') !== -1) {
throw new SoloError(`access denied for path: ${input}`);
}
return input;
}
/**
* Create a map of node aliases to account IDs
* @param nodeAliases
* @returns the map of node IDs to account IDs
*/
export function getNodeAccountMap(nodeAliases) {
const accountMap = new Map();
const realm = constants.HEDERA_NODE_ACCOUNT_ID_START.realm;
const shard = constants.HEDERA_NODE_ACCOUNT_ID_START.shard;
let accountId = constants.HEDERA_NODE_ACCOUNT_ID_START.num;
nodeAliases.forEach(nodeAlias => {
const nodeAccount = `${realm}.${shard}.${accountId}`;
accountId = accountId.add(1);
accountMap.set(nodeAlias, nodeAccount);
});
return accountMap;
}
export function getEnvValue(envVarArray, name) {
const kvPair = envVarArray.find(v => v.startsWith(`${name}=`));
return kvPair ? kvPair.split('=')[1] : null;
}
export function parseIpAddressToUint8Array(ipAddress) {
const parts = ipAddress.split('.');
const uint8Array = new Uint8Array(4);
for (let i = 0; i < 4; i++) {
uint8Array[i] = parseInt(parts[i], 10);
}
return uint8Array;
}
/** If the basename of the src did not match expected basename, rename it first, then copy to destination */
export function renameAndCopyFile(srcFilePath, expectedBaseName, destDir, logger) {
const srcDir = path.dirname(srcFilePath);
if (path.basename(srcFilePath) !== expectedBaseName) {
fs.renameSync(srcFilePath, path.join(srcDir, expectedBaseName));
}
// copy public key and private key to key directory
fs.copyFile(path.join(srcDir, expectedBaseName), path.join(destDir, expectedBaseName), err => {
if (err) {
// @ts-ignore
logger.error(`Error copying file: ${err.message}`);
throw new SoloError(`Error copying file: ${err.message}`);
}
});
}
/**
* Add debug options to valuesArg used by helm chart
* @param valuesArg the valuesArg to update
* @param debugNodeAlias the node ID to attach the debugger to
* @param index the index of extraEnv to add the debug options to
* @returns updated valuesArg
*/
export function addDebugOptions(valuesArg, debugNodeAlias, index = 0) {
if (debugNodeAlias) {
const nodeId = Templates.nodeIdFromNodeAlias(debugNodeAlias);
valuesArg += ` --set "hedera.nodes[${nodeId}].root.extraEnv[${index}].name=JAVA_OPTS"`;
valuesArg += ` --set "hedera.nodes[${nodeId}].root.extraEnv[${index}].value=-agentlib:jdwp=transport=dt_socket\\,server=y\\,suspend=y\\,address=*:${constants.JVM_DEBUG_PORT}"`;
}
return valuesArg;
}
/**
* Returns an object that can be written to a file without data loss.
* Contains fields needed for adding a new node through separate commands
* @param ctx
* @returns file writable object
*/
export function addSaveContextParser(ctx) {
const exportedCtx = {};
const config = ctx.config;
const exportedFields = ['tlsCertHash', 'upgradeZipHash', 'newNode'];
exportedCtx.signingCertDer = ctx.signingCertDer.toString();
exportedCtx.gossipEndpoints = ctx.gossipEndpoints.map((ep) => `${ep.getDomainName}:${ep.getPort}`);
exportedCtx.grpcServiceEndpoints = ctx.grpcServiceEndpoints.map((ep) => `${ep.getDomainName}:${ep.getPort}`);
exportedCtx.adminKey = ctx.adminKey.toString();
// @ts-ignore
exportedCtx.existingNodeAliases = config.existingNodeAliases;
for (const prop of exportedFields) {
exportedCtx[prop] = ctx[prop];
}
return exportedCtx;
}
/**
* Initializes objects in the context from a provided string
* Contains fields needed for adding a new node through separate commands
* @param ctx - accumulator object
* @param ctxData - data in string format
* @returns file writable object
*/
export function addLoadContextParser(ctx, ctxData) {
const config = ctx.config;
ctx.signingCertDer = new Uint8Array(ctxData.signingCertDer.split(','));
ctx.gossipEndpoints = prepareEndpoints(ctx.config.endpointType, ctxData.gossipEndpoints, constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT);
ctx.grpcServiceEndpoints = prepareEndpoints(ctx.config.endpointType, ctxData.grpcServiceEndpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT);
ctx.adminKey = PrivateKey.fromStringED25519(ctxData.adminKey);
config.nodeAlias = ctxData.newNode.name;
config.existingNodeAliases = ctxData.existingNodeAliases;
config.allNodeAliases = [...config.existingNodeAliases, ctxData.newNode.name];
const fieldsToImport = ['tlsCertHash', 'upgradeZipHash', 'newNode'];
for (const prop of fieldsToImport) {
ctx[prop] = ctxData[prop];
}
}
export function prepareEndpoints(endpointType, endpoints, defaultPort) {
const ret = [];
for (const endpoint of endpoints) {
const parts = endpoint.split(':');
let url = '';
let port = defaultPort;
if (parts.length === 2) {
url = parts[0].trim();
port = +parts[1].trim();
}
else if (parts.length === 1) {
url = parts[0];
}
else {
throw new SoloError(`incorrect endpoint format. expected url:port, found ${endpoint}`);
}
if (endpointType.toUpperCase() === constants.ENDPOINT_TYPE_IP) {
ret.push(new ServiceEndpoint({
port: +port,
ipAddressV4: parseIpAddressToUint8Array(url),
}));
}
else {
ret.push(new ServiceEndpoint({
port: +port,
domainName: url,
}));
}
}
return ret;
}
/** Adds all the types of flags as properties on the provided argv object */
export function addFlagsToArgv(argv, flags) {
argv.requiredFlags = flags.requiredFlags;
argv.requiredFlagsWithDisabledPrompt = flags.requiredFlagsWithDisabledPrompt;
argv.optionalFlags = flags.optionalFlags;
return argv;
}
export function resolveValidJsonFilePath(filePath, defaultPath) {
if (!filePath) {
if (defaultPath) {
return resolveValidJsonFilePath(defaultPath, null);
}
return '';
}
const resolvedFilePath = fs.realpathSync(validatePath(filePath));
if (!fs.existsSync(resolvedFilePath)) {
if (defaultPath) {
return resolveValidJsonFilePath(defaultPath, null);
}
throw new SoloError(`File does not exist: ${filePath}`);
}
// If the file is empty (or size cannot be determined) then fallback on the default values
const throttleInfo = fs.statSync(resolvedFilePath);
if (throttleInfo.size === 0 && defaultPath) {
return resolveValidJsonFilePath(defaultPath, null);
}
else if (throttleInfo.size === 0) {
throw new SoloError(`File is empty: ${filePath}`);
}
try {
// Ensure the file contains valid JSON data
JSON.parse(fs.readFileSync(resolvedFilePath, 'utf8'));
return resolvedFilePath;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
// Fallback to the default values if an error occurs due to invalid JSON data or unable to read the file size
if (defaultPath) {
return resolveValidJsonFilePath(defaultPath, null);
}
throw new SoloError(`Invalid JSON data in file: ${filePath}`);
}
}
export function populateHelmArgs(valuesMapping) {
let valuesArg = '';
for (const [key, value] of Object.entries(valuesMapping)) {
valuesArg += ` --set ${key}=${value}`;
}
return valuesArg;
}
/**
* @param nodeAlias
* @param consensusNodes
* @returns context of the node
*/
export function extractContextFromConsensusNodes(nodeAlias, consensusNodes) {
if (!consensusNodes)
return undefined;
if (!consensusNodes.length)
return undefined;
const consensusNode = consensusNodes.find(node => node.name === nodeAlias);
return consensusNode ? consensusNode.context : undefined;
}
export function getSoloVersion() {
if (process.env.npm_package_version) {
return process.env.npm_package_version;
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJsonPath = path.resolve(__dirname, '../../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version;
}
//# sourceMappingURL=helpers.js.map