UNPKG

@hashgraph/solo

Version:

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

283 lines (238 loc) 9.95 kB
/** * 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}`; } }