@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
283 lines (238 loc) • 9.95 kB
text/typescript
/**
* SPDX-License-Identifier: Apache-2.0
*/
import * as x509 from '@peculiar/x509';
import os from 'os';
import path from 'path';
import {DataValidationError, IllegalArgumentError, MissingArgumentError, SoloError} from './errors.js';
import * as constants from './constants.js';
import {type AccountId} from '@hashgraph/sdk';
import {type IP, type NodeAlias, type NodeId} from '../types/aliases.js';
import {PodName} from './kube/resources/pod/pod_name.js';
import {GrpcProxyTlsEnums} from './enumerations.js';
import {HEDERA_PLATFORM_VERSION} from '../../version.js';
import {type NamespaceName} from './kube/resources/namespace/namespace_name.js';
import {type ClusterRef, type NamespaceNameAsString} from './config/remote/types.js';
export class Templates {
public static renderNetworkPodName(nodeAlias: NodeAlias): PodName {
return PodName.of(`network-${nodeAlias}-0`);
}
private static renderNetworkSvcName(nodeAlias: NodeAlias): string {
return `network-${nodeAlias}-svc`;
}
private static nodeAliasFromNetworkSvcName(svcName: string): NodeAlias {
return svcName.split('-').slice(1, -1).join('-') as NodeAlias;
}
public static renderNetworkHeadlessSvcName(nodeAlias: NodeAlias): string {
return `network-${nodeAlias}`;
}
public static renderGossipPemPrivateKeyFile(nodeAlias: NodeAlias): string {
return `${constants.SIGNING_KEY_PREFIX}-private-${nodeAlias}.pem`;
}
public static renderGossipPemPublicKeyFile(nodeAlias: NodeAlias): string {
return `${constants.SIGNING_KEY_PREFIX}-public-${nodeAlias}.pem`;
}
public static renderTLSPemPrivateKeyFile(nodeAlias: NodeAlias): string {
return `hedera-${nodeAlias}.key`;
}
public static renderTLSPemPublicKeyFile(nodeAlias: NodeAlias): string {
return `hedera-${nodeAlias}.crt`;
}
public static renderNodeAdminKeyName(nodeAlias: NodeAlias): string {
return `${nodeAlias}-admin`;
}
public static renderNodeFriendlyName(prefix: string, nodeAlias: NodeAlias, suffix = ''): string {
const parts = [prefix, nodeAlias];
if (suffix) parts.push(suffix);
return parts.join('-');
}
private static extractNodeAliasFromPodName(podName: PodName): NodeAlias {
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() as NodeAlias;
}
static prepareReleasePrefix(tag: string): string {
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
*/
public static renderAccountKeySecretName(accountId: AccountId | string): string {
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 */
public static renderAccountKeySecretLabelSelector(accountId: AccountId | string): string {
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
*/
public static renderAccountKeySecretLabelObject(accountId: AccountId | string): {
'solo.hedera.com/account-id': string;
} {
return {
'solo.hedera.com/account-id': accountId.toString(),
};
}
static renderDistinguishedName(
nodeAlias: 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}`);
}
public static renderStagingDir(cacheDir: string, releaseTagOverride: string): string {
let releaseTag = releaseTagOverride;
if (!cacheDir) {
throw new IllegalArgumentError('cacheDir cannot be empty');
}
if (!releaseTag) {
releaseTag = HEDERA_PLATFORM_VERSION;
}
const releasePrefix = this.prepareReleasePrefix(releaseTag);
if (!releasePrefix) {
throw new IllegalArgumentError('releasePrefix cannot be empty');
}
return path.resolve(path.join(cacheDir, releasePrefix, 'staging', releaseTag));
}
public static installationPath(
dep: string,
osPlatform: NodeJS.Platform | string = os.platform(),
installationDir: string = path.join(constants.SOLO_HOME_DIR, 'bin'),
) {
switch (dep) {
case constants.HELM:
if (osPlatform === constants.OS_WINDOWS) {
return path.join(installationDir, `${dep}.exe`);
}
return path.join(installationDir, dep);
default:
throw new SoloError(`unknown dep: ${dep}`);
}
}
public static renderFullyQualifiedNetworkPodName(namespace: NamespaceName, nodeAlias: NodeAlias): string {
return `${Templates.renderNetworkPodName(nodeAlias)}.${Templates.renderNetworkHeadlessSvcName(nodeAlias)}.${namespace.name}.svc.cluster.local`;
}
public static renderFullyQualifiedNetworkSvcName(namespace: NamespaceName, nodeAlias: NodeAlias): string {
return `${Templates.renderNetworkSvcName(nodeAlias)}.${namespace.name}.svc.cluster.local`;
}
private static nodeAliasFromFullyQualifiedNetworkSvcName(svcName: string): NodeAlias {
const parts = svcName.split('.');
return this.nodeAliasFromNetworkSvcName(parts[0]);
}
public static nodeIdFromNodeAlias(nodeAlias: NodeAlias): NodeId {
for (let i = nodeAlias.length - 1; i > 0; i--) {
// @ts-ignore
if (isNaN(nodeAlias[i])) {
return parseInt(nodeAlias.substring(i + 1, nodeAlias.length)) - 1;
}
}
throw new SoloError(`Can't get node id from node ${nodeAlias}`);
}
public static renderGossipKeySecretName(nodeAlias: NodeAlias): string {
return `network-${nodeAlias}-keys-secrets`;
}
public static renderGossipKeySecretLabelObject(nodeAlias: NodeAlias): {'solo.hedera.com/node-name': string} {
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: NodeAlias, type: GrpcProxyTlsEnums) {
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: NodeAlias, type: GrpcProxyTlsEnums) {
switch (type) {
//? HAProxy Proxy
case GrpcProxyTlsEnums.GRPC:
return {'haproxy-proxy-secret': nodeAlias};
//? Envoy Proxy
case GrpcProxyTlsEnums.GRPC_WEB:
return {'envoy-proxy-secret': nodeAlias};
}
}
public static renderEnvoyProxyName(nodeAlias: NodeAlias): string {
return `envoy-proxy-${nodeAlias}`;
}
public static renderHaProxyName(nodeAlias: NodeAlias): string {
return `haproxy-${nodeAlias}`;
}
public static renderFullyQualifiedHaProxyName(nodeAlias: NodeAlias, namespace: NamespaceName): string {
return `${Templates.renderHaProxyName(nodeAlias)}-svc.${namespace}.svc.cluster.local`;
}
public static parseNodeAliasToIpMapping(unparsed: string): Record<NodeAlias, IP> {
const mapping: Record<NodeAlias, IP> = {};
unparsed.split(',').forEach(data => {
const [nodeAlias, ip] = data.split('=') as [NodeAlias, IP];
mapping[nodeAlias] = ip;
});
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
*/
// TODO @Lenin, needs testing
static renderConsensusNodeFullyQualifiedDomainName(
nodeAlias: string,
nodeId: number,
namespace: NamespaceNameAsString,
cluster: ClusterRef,
dnsBaseDomain: string,
dnsConsensusNodePattern: string,
) {
const searchReplace = {
'${nodeAlias}': nodeAlias,
'${nodeId}': nodeId.toString(),
'${namespace}': namespace,
'${cluster}': cluster,
};
Object.entries(searchReplace).forEach(([search, replace]) => {
dnsConsensusNodePattern = dnsConsensusNodePattern.replace(search, replace);
});
return `${dnsConsensusNodePattern}.${dnsBaseDomain}`;
}
}