UNPKG

@hashgraph/solo

Version:

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

350 lines 16.8 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import { Listr } from 'listr2'; import { MissingArgumentError, SoloError } from '../core/errors.js'; import * as helpers from '../core/helpers.js'; import { getNodeAccountMap } from '../core/helpers.js'; import * as constants from '../core/constants.js'; import { BaseCommand } from './base.js'; import { Flags as flags } from './flags.js'; import { resolveNamespaceFromDeployment } from '../core/resolvers.js'; import { ListrLease } from '../core/lease/listr_lease.js'; import { RelayComponent } from '../core/config/remote/components/relay_component.js'; import { ComponentType } from '../core/config/remote/enumerations.js'; import * as Base64 from 'js-base64'; import { NamespaceName } from '../core/kube/resources/namespace/namespace_name.js'; export class RelayCommand extends BaseCommand { profileManager; accountManager; constructor(opts) { super(opts); if (!opts || !opts.profileManager) throw new MissingArgumentError('An instance of core/ProfileManager is required', opts.downloader); this.profileManager = opts.profileManager; this.accountManager = opts.accountManager; } static get DEPLOY_CONFIGS_NAME() { return 'deployConfigs'; } static get DEPLOY_FLAGS_LIST() { return [ flags.chainId, flags.chartDirectory, flags.clusterRef, flags.deployment, flags.nodeAliasesUnparsed, flags.operatorId, flags.operatorKey, flags.profileFile, flags.profileName, flags.quiet, flags.relayReleaseTag, flags.replicaCount, flags.valuesFile, ]; } static get DESTROY_FLAGS_LIST() { return [flags.chartDirectory, flags.deployment, flags.nodeAliasesUnparsed]; } async prepareValuesArg(valuesFile, nodeAliases, chainID, relayRelease, replicaCount, operatorID, operatorKey, namespace, context) { let valuesArg = ''; const profileName = this.configManager.getFlag(flags.profileName); const profileValuesFile = await this.profileManager.prepareValuesForRpcRelayChart(profileName); if (profileValuesFile) { valuesArg += this.prepareValuesFiles(profileValuesFile); } valuesArg += ` --set config.MIRROR_NODE_URL=http://${constants.MIRROR_NODE_RELEASE_NAME}-rest`; valuesArg += ` --set config.MIRROR_NODE_URL_WEB3=http://${constants.MIRROR_NODE_RELEASE_NAME}-web3`; valuesArg += ' --set config.MIRROR_NODE_AGENT_CACHEABLE_DNS=false'; valuesArg += ' --set config.MIRROR_NODE_RETRY_DELAY=2001'; valuesArg += ' --set config.MIRROR_NODE_GET_CONTRACT_RESULTS_DEFAULT_RETRIES=21'; if (chainID) { valuesArg += ` --set config.CHAIN_ID=${chainID}`; } if (relayRelease) { valuesArg += ` --set image.tag=${relayRelease.replace(/^v/, '')}`; } if (replicaCount) { valuesArg += ` --set replicaCount=${replicaCount}`; } const operatorIdUsing = operatorID || constants.OPERATOR_ID; valuesArg += ` --set config.OPERATOR_ID_MAIN=${operatorIdUsing}`; if (operatorKey) { // use user provided operatorKey if available valuesArg += ` --set config.OPERATOR_KEY_MAIN=${operatorKey}`; } else { try { const deploymentName = this.configManager.getFlag(flags.deployment); const namespace = NamespaceName.of(this.localConfig.deployments[deploymentName].namespace); const k8 = this.k8Factory.getK8(context); const secrets = await k8.secrets().list(namespace, [`solo.hedera.com/account-id=${operatorIdUsing}`]); if (secrets.length === 0) { this.logger.info(`No k8s secret found for operator account id ${operatorIdUsing}, use default one`); valuesArg += ` --set config.OPERATOR_KEY_MAIN=${constants.OPERATOR_KEY}`; } else { this.logger.info('Using operator key from k8s secret'); const operatorKeyFromK8 = Base64.decode(secrets[0].data.privateKey); valuesArg += ` --set config.OPERATOR_KEY_MAIN=${operatorKeyFromK8}`; } } catch (e) { throw new SoloError(`Error getting operator key: ${e.message}`, e); } } if (!nodeAliases) { throw new MissingArgumentError('Node IDs must be specified'); } const networkJsonString = await this.prepareNetworkJsonString(nodeAliases, namespace); valuesArg += ` --set config.HEDERA_NETWORK='${networkJsonString}'`; if (valuesFile) { valuesArg += this.prepareValuesFiles(valuesFile); } return valuesArg; } /** * created a json string to represent the map between the node keys and their ids * output example '{"node-1": "0.0.3", "node-2": "0.004"}' */ async prepareNetworkJsonString(nodeAliases = [], namespace) { if (!nodeAliases) { throw new MissingArgumentError('Node IDs must be specified'); } const networkIds = {}; const accountMap = getNodeAccountMap(nodeAliases); const deploymentName = this.configManager.getFlag(flags.deployment); const networkNodeServicesMap = await this.accountManager.getNodeServiceMap(namespace, this.getClusterRefs(), deploymentName); nodeAliases.forEach(nodeAlias => { const haProxyClusterIp = networkNodeServicesMap.get(nodeAlias).haProxyClusterIp; const haProxyGrpcPort = networkNodeServicesMap.get(nodeAlias).haProxyGrpcPort; const networkKey = `${haProxyClusterIp}:${haProxyGrpcPort}`; networkIds[networkKey] = accountMap.get(nodeAlias); }); return JSON.stringify(networkIds); } prepareReleaseName(nodeAliases = []) { if (!nodeAliases) { throw new MissingArgumentError('Node IDs must be specified'); } let releaseName = 'relay'; nodeAliases.forEach(nodeAlias => { releaseName += `-${nodeAlias}`; }); return releaseName; } async deploy(argv) { const self = this; const lease = await self.leaseManager.create(); const tasks = new Listr([ { title: 'Initialize', task: async (ctx, task) => { // reset nodeAlias self.configManager.setFlag(flags.nodeAliasesUnparsed, ''); self.configManager.update(argv); flags.disablePrompts([flags.operatorId, flags.operatorKey, flags.clusterRef]); await self.configManager.executePrompt(task, RelayCommand.DEPLOY_FLAGS_LIST); // prompt if inputs are empty and set it in the context ctx.config = this.getConfig(RelayCommand.DEPLOY_CONFIGS_NAME, RelayCommand.DEPLOY_FLAGS_LIST, [ 'nodeAliases', ]); ctx.config.namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task); ctx.config.nodeAliases = helpers.parseNodeAliases(ctx.config.nodeAliasesUnparsed); ctx.config.releaseName = self.prepareReleaseName(ctx.config.nodeAliases); if (ctx.config.clusterRef) { const context = self.getClusterRefs()[ctx.config.clusterRef]; if (context) ctx.config.context = context; } ctx.config.isChartInstalled = await self.chartManager.isChartInstalled(ctx.config.namespace, ctx.config.releaseName, ctx.config.context); self.logger.debug('Initialized config', { config: ctx.config }); return ListrLease.newAcquireLeaseTask(lease, task); }, }, { title: 'Prepare chart values', task: async (ctx) => { const config = ctx.config; config.chartPath = await self.prepareChartPath(config.chartDirectory, constants.JSON_RPC_RELAY_CHART, constants.JSON_RPC_RELAY_CHART); await self.accountManager.loadNodeClient(ctx.config.namespace, self.getClusterRefs(), self.configManager.getFlag(flags.deployment), self.configManager.getFlag(flags.forcePortForward), ctx.config.context); config.valuesArg = await self.prepareValuesArg(config.valuesFile, config.nodeAliases, config.chainId, config.relayReleaseTag, config.replicaCount, config.operatorId, config.operatorKey, config.namespace, config.context); }, }, { title: 'Deploy JSON RPC Relay', task: async (ctx) => { const config = ctx.config; const k8 = self.k8Factory.getK8(config.context); const kubeContext = k8.contexts().readCurrent(); await self.chartManager.install(config.namespace, config.releaseName, config.chartPath, '', config.valuesArg, kubeContext); await k8 .pods() .waitForRunningPhase(config.namespace, ['app=hedera-json-rpc-relay', `app.kubernetes.io/instance=${config.releaseName}`], constants.RELAY_PODS_RUNNING_MAX_ATTEMPTS, constants.RELAY_PODS_RUNNING_DELAY); // reset nodeAlias self.configManager.setFlag(flags.nodeAliasesUnparsed, ''); }, }, { title: 'Check relay is ready', task: async (ctx) => { const config = ctx.config; const k8 = self.k8Factory.getK8(config.context); try { await k8 .pods() .waitForReadyStatus(config.namespace, ['app=hedera-json-rpc-relay', `app.kubernetes.io/instance=${config.releaseName}`], constants.RELAY_PODS_READY_MAX_ATTEMPTS, constants.RELAY_PODS_READY_DELAY); } catch (e) { throw new SoloError(`Relay ${config.releaseName} is not ready: ${e.message}`, e); } }, }, this.addRelayComponent(), ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); try { await tasks.run(); } catch (e) { throw new SoloError('Error installing relays', e); } finally { await lease.release(); await self.accountManager.close(); } return true; } async destroy(argv) { const self = this; const lease = await self.leaseManager.create(); const tasks = new Listr([ { title: 'Initialize', task: async (ctx, task) => { // reset nodeAlias self.configManager.setFlag(flags.nodeAliasesUnparsed, ''); self.configManager.update(argv); await self.configManager.executePrompt(task, RelayCommand.DESTROY_FLAGS_LIST); const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task); // prompt if inputs are empty and set it in the context ctx.config = { chartDirectory: self.configManager.getFlag(flags.chartDirectory), namespace: namespace, nodeAliases: helpers.parseNodeAliases(self.configManager.getFlag(flags.nodeAliasesUnparsed)), }; ctx.config.releaseName = this.prepareReleaseName(ctx.config.nodeAliases); ctx.config.isChartInstalled = await this.chartManager.isChartInstalled(ctx.config.namespace, ctx.config.releaseName); self.logger.debug('Initialized config', { config: ctx.config }); return ListrLease.newAcquireLeaseTask(lease, task); }, }, { title: 'Destroy JSON RPC Relay', task: async (ctx) => { const config = ctx.config; await this.chartManager.uninstall(config.namespace, config.releaseName, this.k8Factory.default().contexts().readCurrent()); this.logger.showList('Destroyed Relays', await self.chartManager.getInstalledCharts(config.namespace)); // reset nodeAliasesUnparsed self.configManager.setFlag(flags.nodeAliasesUnparsed, ''); }, skip: ctx => !ctx.config.isChartInstalled, }, this.removeRelayComponent(), ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); try { await tasks.run(); } catch (e) { throw new SoloError('Error uninstalling relays', e); } finally { await lease.release(); } return true; } getCommandDefinition() { const self = this; return { command: 'relay', desc: 'Manage JSON RPC relays in solo network', builder: (yargs) => { return yargs .command({ command: 'deploy', desc: 'Deploy a JSON RPC relay', builder: (y) => { flags.setCommandFlags(y, ...RelayCommand.DEPLOY_FLAGS_LIST); }, handler: (argv) => { self.logger.info("==== Running 'relay deploy' ===", { argv }); self.logger.info(argv); self .deploy(argv) .then(r => { self.logger.info('==== Finished running `relay deploy`===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .command({ command: 'destroy', desc: 'Destroy JSON RPC relay', builder: (y) => flags.setCommandFlags(y, flags.chartDirectory, flags.deployment, flags.quiet, flags.nodeAliasesUnparsed), handler: (argv) => { self.logger.info("==== Running 'relay destroy' ===", { argv }); self.logger.debug(argv); self.destroy(argv).then(r => { self.logger.info('==== Finished running `relay destroy`===='); if (!r) process.exit(1); }); }, }) .demandCommand(1, 'Select a relay command'); }, }; } /** Adds the relay component to remote config. */ addRelayComponent() { return { title: 'Add relay component in remote config', skip: () => !this.remoteConfigManager.isLoaded(), task: async (ctx) => { await this.remoteConfigManager.modify(async (remoteConfig) => { const { config: { namespace, nodeAliases }, } = ctx; const cluster = this.remoteConfigManager.currentCluster; remoteConfig.components.add('relay', new RelayComponent('relay', cluster, namespace.name, nodeAliases)); }); }, }; } /** Remove the relay component from remote config. */ removeRelayComponent() { return { title: 'Remove relay component from remote config', skip: () => !this.remoteConfigManager.isLoaded(), task: async () => { await this.remoteConfigManager.modify(async (remoteConfig) => { remoteConfig.components.remove('relay', ComponentType.Relay); }); }, }; } close() { // no-op return Promise.resolve(); } } //# sourceMappingURL=relay.js.map