UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

353 lines 14.2 kB
/** * 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