@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
444 lines • 18.9 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import * as x509 from '@peculiar/x509';
import { DataValidationError } from './errors/data-validation-error.js';
import { IllegalArgumentError } from './errors/illegal-argument-error.js';
import { MissingArgumentError } from './errors/missing-argument-error.js';
import { SoloError } from './errors/solo-error.js';
import * as constants from './constants.js';
import { PodName } from '../integration/kube/resources/pod/pod-name.js';
import { GrpcProxyTlsEnums } from './enumerations.js';
import { PathEx } from '../business/utils/path-ex.js';
import { HEDERA_PLATFORM_VERSION } from '../../version.js';
import { OperatingSystem } from '../business/utils/operating-system.js';
export class Templates {
static renderNetworkPodName(nodeAlias) {
return PodName.of(`network-${nodeAlias}-0`);
}
static renderNetworkSvcName(nodeAlias) {
return `network-${nodeAlias}-svc`;
}
static renderNetworkHeadlessSvcName(nodeAlias) {
return `network-${nodeAlias}`;
}
static renderNodeAliasFromNumber(number_) {
return `node${number_}`;
}
static renderPostgresPodName(number_) {
return PodName.of(`solo-shared-resources-postgres-${number_}`);
}
static renderNodeAliasesFromCount(count, existingNodesCount) {
const nodeAliases = [];
let nodeNumber = existingNodesCount + 1;
for (let index = 1; index <= count; index++) {
nodeAliases.push(Templates.renderNodeAliasFromNumber(nodeNumber));
nodeNumber++;
}
return nodeAliases;
}
static renderMirrorNodeDatabaseInitScriptUrl(release) {
return `https://raw.githubusercontent.com/hiero-ledger/hiero-mirror-node/refs/tags/${release}/importer/src/main/resources/db/scripts/init.sh`;
}
static renderGossipPemPrivateKeyFile(nodeAlias) {
return `${constants.SIGNING_KEY_PREFIX}-private-${nodeAlias}.pem`;
}
static renderGossipPemPublicKeyFile(nodeAlias) {
return `${constants.SIGNING_KEY_PREFIX}-public-${nodeAlias}.pem`;
}
static renderTLSPemPrivateKeyFile(nodeAlias) {
return `hedera-${nodeAlias}.key`;
}
static renderTLSPemPublicKeyFile(nodeAlias) {
return `hedera-${nodeAlias}.crt`;
}
static renderNodeAdminKeyName(nodeAlias) {
return `${nodeAlias}-admin`;
}
static renderNodeFriendlyName(prefix, nodeAlias, suffix = '') {
const parts = [prefix, nodeAlias];
if (suffix) {
parts.push(suffix);
}
return parts.join('-');
}
static extractNodeAliasFromPodName(podName) {
const parts = podName.name.split('-');
if (parts.length !== 3) {
throw new DataValidationError(`pod name is malformed : ${podName.name}`, 3, parts.length);
}
return parts[1].trim();
}
static prepareReleasePrefix(tag) {
if (!tag) {
throw new MissingArgumentError('tag cannot be empty');
}
const parsed = tag.split('.');
if (parsed.length < 3) {
throw new Error(`tag (${tag}) must include major, minor and patch fields (e.g. v0.40.4)`);
}
return `${parsed[0]}.${parsed[1]}`;
}
/**
* renders the name to be used to store the new account key as a Kubernetes secret
* @param accountId
* @returns the name of the Kubernetes secret to store the account key
*/
static renderAccountKeySecretName(accountId) {
return `account-key-${accountId.toString()}`;
}
/**
* renders the label selector to be used to fetch the new account key from the Kubernetes secret
* @param accountId
* @returns the label selector of the Kubernetes secret to retrieve the account key */
static renderAccountKeySecretLabelSelector(accountId) {
return `solo.hedera.com/account-id=${accountId.toString()}`;
}
/**
* renders the label object to be used to store the new account key in the Kubernetes secret
* @param accountId
* @returns the label object to be used to store the new account key in the Kubernetes secret
*/
static renderAccountKeySecretLabelObject(accountId) {
return {
'solo.hedera.com/account-id': accountId.toString(),
};
}
static renderDistinguishedName(nodeAlias, state = 'TX', locality = 'Richardson', org = 'Hedera', orgUnit = 'Hedera', country = 'US') {
return new x509.Name(`CN=${nodeAlias},ST=${state},L=${locality},O=${org},OU=${orgUnit},C=${country}`);
}
static renderStagingDir(cacheDirectory, releaseTagOverride) {
let releaseTag = releaseTagOverride;
if (!cacheDirectory) {
throw new IllegalArgumentError('cacheDirectory cannot be empty');
}
if (!releaseTag) {
releaseTag = HEDERA_PLATFORM_VERSION;
}
const releasePrefix = this.prepareReleasePrefix(releaseTag);
if (!releasePrefix) {
throw new IllegalArgumentError('releasePrefix cannot be empty');
}
return PathEx.resolve(PathEx.join(cacheDirectory, releasePrefix, 'staging', releaseTag));
}
static localInstallationExecutableForDependency(dependency, installationDirectory = PathEx.join(constants.SOLO_HOME_DIR, 'bin')) {
switch (dependency) {
case constants.HELM:
case constants.KIND:
case constants.PODMAN:
case constants.VFKIT:
case constants.GVPROXY:
case constants.CRANE:
case constants.KUBECTL: {
if (OperatingSystem.isWin32()) {
return PathEx.join(installationDirectory, `${dependency}.exe`);
}
return PathEx.join(installationDirectory, dependency);
}
default: {
throw new SoloError(`unknown dependency: ${dependency}`);
}
}
}
static renderFullyQualifiedNetworkPodName(namespace, nodeAlias) {
return `${Templates.renderNetworkPodName(nodeAlias)}.${Templates.renderNetworkHeadlessSvcName(nodeAlias)}.${namespace.name}.svc.cluster.local`;
}
static renderFullyQualifiedNetworkSvcName(namespace, nodeAlias) {
return `${Templates.renderNetworkSvcName(nodeAlias)}.${namespace.name}.svc.cluster.local`;
}
static nodeIdFromNodeAlias(nodeAlias) {
for (let index = nodeAlias.length - 1; index > 0; index--) {
if (Number.isNaN(Number.parseInt(nodeAlias[index]))) {
return Number.parseInt(nodeAlias.slice(index + 1)) - 1;
}
}
throw new SoloError(`Can't get node id from node ${nodeAlias}`);
}
static renderComponentIdFromNodeId(nodeId) {
return nodeId + 1;
}
static renderComponentIdFromNodeAlias(nodeAlias) {
return this.nodeIdFromNodeAlias(nodeAlias) + 1;
}
static renderNodeIdFromComponentId(componentId) {
return componentId - 1;
}
static renderGossipKeySecretName(nodeAlias) {
return `network-${nodeAlias}-keys-secrets`;
}
static renderGossipKeySecretLabelObject(nodeAlias) {
return { 'solo.hedera.com/node-name': nodeAlias };
}
/**
* Creates the secret name based on the node alias type
*
* @param nodeAlias - node alias
* @param type - whether is for gRPC or gRPC Web ( Haproxy or Envoy )
*
* @returns the appropriate secret name
*/
static renderGrpcTlsCertificatesSecretName(nodeAlias, type) {
switch (type) {
//? HAProxy Proxy
case GrpcProxyTlsEnums.GRPC: {
return `haproxy-proxy-secret-${nodeAlias}`;
}
//? Envoy Proxy
case GrpcProxyTlsEnums.GRPC_WEB: {
return `envoy-proxy-secret-${nodeAlias}`;
}
}
}
/**
* Creates the secret labels based on the node alias type
*
* @param nodeAlias - node alias
* @param type - whether is for gRPC or gRPC Web ( Haproxy or Envoy )
*
* @returns the appropriate secret labels
*/
static renderGrpcTlsCertificatesSecretLabelObject(nodeAlias, type) {
switch (type) {
//? HAProxy Proxy
case GrpcProxyTlsEnums.GRPC: {
return { 'haproxy-proxy-secret': nodeAlias };
}
//? Envoy Proxy
case GrpcProxyTlsEnums.GRPC_WEB: {
return { 'envoy-proxy-secret': nodeAlias };
}
}
}
static parseNodeAliasToIpMapping(unparsed) {
const mapping = {};
for (const data of unparsed.split(',')) {
const [nodeAlias, ip] = data.split('=');
mapping[nodeAlias] = ip;
}
return mapping;
}
/**
* Parses a comma-separated string into a mapping of node aliases → address/port.
*
* Accepted input formats:
* 1) Explicit alias → address[:port]
* Each entry provides the node alias and the target address, optionally with a port.
* Example: "node1=127.0.0.1:8080,node2=127.0.0.1:8081"
*
* 2) Explicit alias → address (no port)
* Same as above, but if the port is omitted it defaults to 8080.
* Example: "node1=localhost,node2=localhost:8081"
*
* 3) Address[:port] only (no aliases)
* Aliases are inferred from the `nodes` array by index order.
* If the port is omitted, it defaults to 8080.
* Example: "localhost,127.0.0.2:8081"
*
* @param unparsed - Input string describing alias/address[:port] mappings.
* @param nodes - Used to infer aliases when not explicitly provided.
* @returns Record keyed by NodeAlias with resolved address and port.
*
* @throws SoloError if an alias cannot be inferred.
*/
static parseNodeAliasToAddressAndPortMapping(unparsed, nodes) {
const mapping = {};
if (!unparsed || typeof unparsed !== 'string') {
return mapping;
}
for (const [index, data] of unparsed.split(',').entries()) {
const [nodeAlias, addressData] = data.includes('=')
? data.split('=')
: [nodes[index]?.name, data];
if (!nodeAlias) {
throw new SoloError(`Node alias for ${addressData} cannot be inferred`);
}
const [address, port] = addressData.includes(':') ? addressData.split(':') : [addressData, '8080'];
mapping[nodeAlias] = { address, port: +port };
}
return mapping;
}
static parseNodeAliasToDomainNameMapping(unparsed) {
const mapping = {};
for (const data of unparsed.split(',')) {
const [nodeAlias, domainName] = data.split('=');
if (!nodeAlias || typeof nodeAlias !== 'string') {
throw new SoloError(`Can't parse node alias: ${data}`);
}
if (!domainName || typeof domainName !== 'string') {
throw new SoloError(`Can't parse domain name: ${data}`);
}
mapping[nodeAlias] = domainName;
}
return mapping;
}
/**
* Renders the fully qualified domain name for a consensus node. We support the following variables for templating
* in the dnsConsensusNodePattern: {nodeAlias}, {nodeId}, {namespace}, {cluster}
*
* The end result will be `${dnsConsensusNodePattern}.${dnsBaseDomain}`.
* For example, if the dnsConsensusNodePattern is `network-{nodeAlias}-svc.{namespace}.svc` and the dnsBaseDomain is `cluster.local`,
* the fully qualified domain name will be `network-{nodeAlias}-svc.{namespace}.svc.cluster.local`.
* @param nodeAlias - the alias of the consensus node
* @param nodeId - the id of the consensus node
* @param namespace - the namespace of the consensus node
* @param cluster - the cluster of the consensus node
* @param dnsBaseDomain - the base domain of the cluster
* @param dnsConsensusNodePattern - the pattern to use for the consensus node
*/
static renderConsensusNodeFullyQualifiedDomainName(nodeAlias, nodeId, namespace, cluster, dnsBaseDomain, dnsConsensusNodePattern) {
const searchReplace = {
'{nodeAlias}': nodeAlias,
'{nodeId}': nodeId.toString(),
'{namespace}': namespace,
'{cluster}': cluster,
};
for (const [search, replace] of Object.entries(searchReplace)) {
dnsConsensusNodePattern = dnsConsensusNodePattern.replace(search, replace);
}
return `${dnsConsensusNodePattern}.${dnsBaseDomain}`;
}
/**
* @param serviceName - name of the service
* @param namespace - the pattern to use for the consensus node
* @param dnsBaseDomain - the base domain of the cluster
*/
static renderSvcFullyQualifiedDomainName(serviceName, namespace, dnsBaseDomain) {
return `${serviceName}.${namespace}.svc.${dnsBaseDomain}`;
}
// Component Label Selectors
static renderRelayLabels(id, legacyReleaseName) {
return legacyReleaseName
? [`app.kubernetes.io/instance=${legacyReleaseName}`, 'app.kubernetes.io/name=relay']
: [`app.kubernetes.io/instance=${constants.JSON_RPC_RELAY_RELEASE_NAME}-${id}`, 'app.kubernetes.io/name=relay'];
}
static renderHaProxyLabels(id) {
const nodeAlias = Templates.renderNodeAliasFromNumber(id);
return [`app=haproxy-${nodeAlias}`, 'solo.hedera.com/type=haproxy'];
}
static renderMirrorNodeLabels(id, legacyReleaseName) {
const releaseName = legacyReleaseName ?? Templates.renderMirrorNodeName(id);
return [
'app.kubernetes.io/name=importer',
'app.kubernetes.io/component=importer',
`app.kubernetes.io/instance=${releaseName}`,
];
}
static renderMirrorIngressControllerLabels() {
return [constants.SOLO_INGRESS_CONTROLLER_NAME_LABEL];
}
static renderEnvoyProxyLabels(id) {
const nodeAlias = Templates.renderNodeAliasFromNumber(id);
return [`solo.hedera.com/node-name=${nodeAlias}`, 'solo.hedera.com/type=envoy-proxy'];
}
static renderExplorerLabels(id, legacyReleaseName) {
const releaseName = legacyReleaseName ?? `${constants.EXPLORER_RELEASE_NAME}-${id}`;
return [`app.kubernetes.io/instance=${releaseName}`];
}
static renderConsensusNodeLabels(id) {
return [`app=network-${Templates.renderNodeAliasFromNumber(id)}`];
}
static renderBlockNodeLabels(id, legacyReleaseName) {
const releaseName = legacyReleaseName ?? Templates.renderBlockNodeName(id);
return [`app.kubernetes.io/name=${releaseName}`];
}
static renderExplorerName(id) {
return `${constants.EXPLORER_RELEASE_NAME}-${id}`;
}
static renderRelayName(id) {
return `${constants.JSON_RPC_RELAY_RELEASE_NAME}-${id}`;
}
static renderBlockNodeName(id) {
return `${constants.BLOCK_NODE_RELEASE_NAME}-${id}`;
}
static renderMirrorNodeName(id) {
return `${constants.MIRROR_NODE_RELEASE_NAME}-${id}`;
}
static renderConfigMapRemoteConfigLabels() {
return ['solo.hedera.com/type=remote-config'];
}
static renderNodeLabelsFromNodeAlias(nodeAlias) {
return [`solo.hedera.com/node-name=${nodeAlias}`, 'solo.hedera.com/type=network-node'];
}
static renderNodeSvcLabelsFromNodeId(nodeId) {
return [`solo.hedera.com/node-id=${nodeId},solo.hedera.com/type=network-node-svc`];
}
/**
* Build label selectors for deployment refresh by component type.
*/
static renderComponentLabelSelectors(componentType, id) {
switch (componentType) {
case 'ConsensusNode': {
return Templates.renderHaProxyLabels(id);
}
case 'HaProxy': {
return Templates.renderHaProxyLabels(id);
}
case 'BlockNode': {
return Templates.renderBlockNodeLabels(id);
}
case 'MirrorNode': {
return Templates.renderMirrorIngressControllerLabels();
}
case 'RelayNode': {
return Templates.renderRelayLabels(id);
}
case 'Explorer': {
return Templates.renderExplorerLabels(id);
}
default: {
return [];
}
}
}
static parseExternalBlockAddress(raw) {
const [address, port] = raw.includes(':') ? raw.split(':') : [raw, constants.BLOCK_NODE_PORT];
return [address, +port];
}
static parseBlockNodePriorityMapping(rawString, nodes) {
const mapping = {};
const isDefault = !rawString || rawString.split(',').length === 0;
const nodeAliasesToPriorityMapping = isDefault
? nodes.map((node) => node.name)
: rawString.split(',');
for (const data of nodeAliasesToPriorityMapping) {
// eslint-disable-next-line prefer-const
let [nodeAlias, priority] = data.split('=');
mapping[nodeAlias] = +priority || 1;
}
return mapping;
}
/**
* @param rawString - the raw string from the unparsed [flags.blockNodeMapping, flags.externalBlockNodeMapping]
* @param fallbackBlockNodeIds - either block node IDs or external block node IDs
*/
static parseConsensusNodePriorityMapping(rawString, fallbackBlockNodeIds) {
// if no nodes are specified use one and set highest priority to first block node
const useDefault = typeof rawString !== 'string' || rawString.length === 0;
if (useDefault) {
const mapping = [];
for (const [index, blockNodeId] of fallbackBlockNodeIds.entries()) {
// set higher priority to first node
mapping.push([blockNodeId, index === 0 ? 2 : 1]);
}
return mapping;
}
// Figure out if any priority is explicitly specified
const hasPriority = rawString.includes('=');
const mapping = [];
for (const [index, data] of rawString.split(',').entries()) {
// use specified priority if set
if (data.includes('=')) {
const [blockNodeId, priority] = data.split('=');
mapping.push([+blockNodeId, +priority]);
}
// if any node has priority specified, default all unset to 1
else if (hasPriority) {
mapping.push([+data, 1]);
}
// if no explicit priority is specified, set higher priority to first node
else {
mapping.push([+data, index === 0 ? 2 : 1]);
}
}
return mapping;
}
}
//# sourceMappingURL=templates.js.map