UNPKG

@hashgraph/solo

Version:

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

1,283 lines (1,132 loc) 50.3 kB
// SPDX-License-Identifier: Apache-2.0 import * as helpers from '../../core/helpers.js'; import * as NodeFlags from './flags.js'; import {type NodeCommandConfigs} from './configs.js'; import * as constants from '../../core/constants.js'; import {type LockManager} from '../../core/lock/lock-manager.js'; import {SoloError} from '../../core/errors/solo-error.js'; import {type Lock} from '../../core/lock/lock.js'; import {LeaseWrapper, type NodeCommandTasks} from './tasks.js'; import {NodeSubcommandType} from '../../core/enumerations.js'; import {NodeHelper} from './helper.js'; import {AnyListrContext, type ArgvStruct, type NodeAlias, type NodeAliases} from '../../types/aliases.js'; import chalk from 'chalk'; import {type ComponentId, type Optional, type SoloListr, type SoloListrTask} from '../../types/index.js'; import {inject, injectable} from 'tsyringe-neo'; import {patchInject} from '../../core/dependency-injection/container-helper.js'; import {CommandHandler} from '../../core/command-handler.js'; import {type NamespaceName} from '../../types/namespace/namespace-name.js'; import {type ConsensusNode} from '../../core/model/consensus-node.js'; import {InjectTokens} from '../../core/dependency-injection/inject-tokens.js'; import {type NodeDestroyContext} from './config-interfaces/node-destroy-context.js'; import {type NodeAddContext} from './config-interfaces/node-add-context.js'; import {type NodeUpdateContext} from './config-interfaces/node-update-context.js'; import {type NodeUpgradeContext} from './config-interfaces/node-upgrade-context.js'; import {ComponentTypes} from '../../core/config/remote/enumerations/component-types.js'; import {DeploymentPhase} from '../../data/schema/model/remote/deployment-phase.js'; import {Templates} from '../../core/templates.js'; import {ConsensusNodeStateSchema} from '../../data/schema/model/remote/state/consensus-node-state-schema.js'; import {type RemoteConfigRuntimeStateApi} from '../../business/runtime-state/api/remote-config-runtime-state-api.js'; import {ComponentsDataWrapperApi} from '../../core/config/remote/api/components-data-wrapper-api.js'; import {LedgerPhase} from '../../data/schema/model/remote/ledger-phase.js'; import {LocalConfigRuntimeState} from '../../business/runtime-state/config/local/local-config-runtime-state.js'; import {type Zippy} from '../../core/zippy.js'; import {PathEx} from '../../business/utils/path-ex.js'; import {Flags as flags} from '../flags.js'; import {select as selectPrompt} from '@inquirer/prompts'; import {Deployment} from '../../business/runtime-state/config/local/deployment.js'; import {MutableFacadeArray} from '../../business/runtime-state/collection/mutable-facade-array.js'; import {DeploymentSchema} from '../../data/schema/model/local/deployment-schema.js'; import {type ConfigManager} from '../../core/config-manager.js'; import {getSoloVersion} from '../../../version.js'; import {DiagnosticsReporter} from '../util/diagnostics-reporter.js'; import {findDeploymentsFromRemoteConfig} from '../util/find-deployments-from-remote-config.js'; import {GetSoloRemoteConfigMapTask} from '../util/get-solo-remote-config-map-task.js'; import {type RemoteDeploymentInfo} from '../util/remote-deployment-info.js'; import {type K8Factory} from '../../integration/kube/k8-factory.js'; @injectable() export class NodeCommandHandlers extends CommandHandler { private readonly nodeConfigManager: ConfigManager; public constructor( @inject(InjectTokens.LockManager) private readonly leaseManager: LockManager, @inject(InjectTokens.ConfigManager) configManager: ConfigManager, @inject(InjectTokens.LocalConfigRuntimeState) private readonly localConfig: LocalConfigRuntimeState, @inject(InjectTokens.RemoteConfigRuntimeState) private readonly remoteConfig: RemoteConfigRuntimeStateApi, @inject(InjectTokens.NodeCommandTasks) private readonly tasks: NodeCommandTasks, @inject(InjectTokens.NodeCommandConfigs) private readonly configs: NodeCommandConfigs, @inject(InjectTokens.K8Factory) private readonly k8Factory: K8Factory, @inject(InjectTokens.Zippy) private readonly zippy?: Zippy, ) { super(); this.leaseManager = patchInject(leaseManager, InjectTokens.LockManager, this.constructor.name); this.nodeConfigManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name); this.configs = patchInject(configs, InjectTokens.NodeCommandConfigs, this.constructor.name); this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name); this.remoteConfig = patchInject(remoteConfig, InjectTokens.RemoteConfigRuntimeState, this.constructor.name); this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name); this.tasks = patchInject(tasks, InjectTokens.NodeCommandTasks, this.constructor.name); this.zippy = patchInject(zippy, InjectTokens.Zippy, this.constructor.name); } private static readonly ADD_CONTEXT_FILE: string = 'node-add.json'; private static readonly DESTROY_CONTEXT_FILE: string = 'node-destroy.json'; private static readonly UPDATE_CONTEXT_FILE: string = 'node-update.json'; private static readonly UPGRADE_CONTEXT_FILE: string = 'node-upgrade.json'; private resolveOutputDirectory(argv: ArgvStruct, fallback: string = ''): string { this.nodeConfigManager.update(argv); return this.nodeConfigManager.getFlag<string>(flags.outputDir) || fallback; } private resolveDeploymentFlag(argv: ArgvStruct): string { const deploymentFromArgument: string = (argv[flags.deployment.name] as string) || ''; if (deploymentFromArgument) { return deploymentFromArgument; } this.nodeConfigManager.update(argv); return this.nodeConfigManager.getFlag<string>(flags.deployment) || ''; } private resolveQuietFlag(argv: ArgvStruct): boolean { if (argv[flags.quiet.name] !== undefined) { return argv[flags.quiet.name] === true; } this.nodeConfigManager.update(argv); return this.nodeConfigManager.getFlag<boolean>(flags.quiet) === true; } private ensureInteractiveSelectionPrompt(): void { if (!process.stdout.isTTY || !process.stdin.isTTY) { throw new SoloError('Cannot prompt for input in non-interactive mode'); } } /** ******** Task Lists **********/ private destroyPrepareTaskList(argv: ArgvStruct, lease: Lock): SoloListrTask<NodeDestroyContext>[] { return [ this.tasks.initialize(argv, this.configs.destroyConfigBuilder.bind(this.configs), lease), this.validateSingleNodeState({excludedPhases: []}), this.tasks.identifyExistingNodes(), this.tasks.loadAdminKey(), this.tasks.prepareUpgradeZip(), this.tasks.checkExistingNodesStakedAmount(), ]; } private destroySubmitTransactionsTaskList(): SoloListrTask<NodeDestroyContext>[] { return [ this.tasks.sendNodeDeleteTransaction(), this.tasks.sendPrepareUpgradeTransaction() as SoloListrTask<NodeDestroyContext>, this.tasks.sendFreezeUpgradeTransaction() as SoloListrTask<NodeDestroyContext>, ]; } private destroyExecuteTaskList(): SoloListrTask<NodeDestroyContext>[] { return [ this.tasks.checkAllNodesAreFrozen('existingNodeAliases'), this.tasks.stopNodes('existingNodeAliases'), this.tasks.downloadNodeGeneratedFilesForDynamicAddressBook(), this.tasks.prepareStagingDirectory('existingNodeAliases'), this.tasks.refreshNodeList(), this.tasks.copyNodeKeysToSecrets('refreshedConsensusNodes'), this.tasks.getNodeLogsAndConfigs(), this.tasks.updateChartWithConfigMap( 'Delete network node from chart and update configMaps', NodeSubcommandType.DESTROY, ), this.tasks.killNodes(NodeSubcommandType.DESTROY), this.tasks.sleep('Give time for pods to come up after being killed', 20_000), this.tasks.checkNodePodsAreRunning(), this.tasks.populateServiceMap(), this.tasks.fetchPlatformSoftware('allNodeAliases'), this.tasks.setupNetworkNodes('allNodeAliases', false), this.tasks.startNodes('allNodeAliases'), this.tasks.enablePortForwarding(), this.tasks.checkAllNodesAreActive('allNodeAliases'), this.tasks.checkAllNodeProxiesAreActive(), this.tasks.triggerStakeWeightCalculate<NodeDestroyContext>(NodeSubcommandType.DESTROY), this.tasks.finalize(), ]; } private addPrepareTasks(argv: ArgvStruct, lease: Lock): SoloListrTask<NodeAddContext>[] { return [ this.tasks.initialize(argv, this.configs.addConfigBuilder.bind(this.configs), lease), // TODO instead of validating the state we need to do a remote config add component, and we will need to manually // the nodeAlias based on the next available node ID + 1 // this.validateSingleNodeState({excludedPhases: []}), this.tasks.checkPVCsEnabled(), this.tasks.identifyExistingNodes(), this.tasks.determineNewNodeAccountNumber(), this.tasks.copyGrpcTlsCertificates(), this.tasks.generateGossipKey(), this.tasks.generateGrpcTlsKey(), this.tasks.loadSigningKeyCertificate(), this.tasks.computeMTLSCertificateHash(), this.tasks.prepareGossipEndpoints(), this.tasks.prepareGrpcServiceEndpoints(), this.tasks.prepareUpgradeZip(), this.tasks.checkExistingNodesStakedAmount(), ]; } private addSubmitTransactionsTasks(): SoloListrTask<NodeAddContext>[] { return [ this.tasks.sendNodeCreateTransaction(), this.tasks.sendPrepareUpgradeTransaction() as SoloListrTask<NodeAddContext>, this.tasks.sendFreezeUpgradeTransaction() as SoloListrTask<NodeAddContext>, ]; } private addExecuteTasks(): SoloListrTask<NodeAddContext>[] { return [ this.tasks.checkAllNodesAreFrozen('existingNodeAliases'), this.tasks.downloadNodeGeneratedFilesForDynamicAddressBook(), this.tasks.prepareStagingDirectory('allNodeAliases'), this.tasks.addNewConsensusNodeToRemoteConfig(), this.tasks.copyNodeKeysToSecrets(), this.tasks.getNodeLogsAndConfigs(), this.tasks.updateChartWithConfigMap('Deploy new network node', NodeSubcommandType.ADD), this.tasks.stopNodes('existingNodeAliases'), this.tasks.killNodes(), this.tasks.checkNodePodsAreRunning(), this.tasks.populateServiceMap(), this.tasks.fetchPlatformSoftware('allNodeAliases'), this.tasks.downloadLastState(), this.tasks.uploadStateToNewNode(), this.tasks.setupNetworkNodes('allNodeAliases', false), this.tasks.updateBlockNodesJson(), this.tasks.addWrapsLib(), this.tasks.startNodes('allNodeAliases'), this.tasks.enablePortForwarding(), this.tasks.checkAllNodesAreActive('allNodeAliases'), this.tasks.checkAllNodeProxiesAreActive(), this.tasks.waitForTss(), this.tasks.stakeNewNode(), this.tasks.triggerStakeWeightCalculate<NodeAddContext>(NodeSubcommandType.ADD), this.tasks.loadAdminKey(), this.tasks.setGrpcWebEndpoint('newNodeAliases', NodeSubcommandType.ADD), this.tasks.finalize(), ]; } private updatePrepareTasks(argv: ArgvStruct, lease: Lock): SoloListrTask<NodeUpdateContext>[] { return [ this.tasks.initialize(argv, this.configs.updateConfigBuilder.bind(this.configs), lease), this.validateSingleNodeState({excludedPhases: []}), this.tasks.identifyExistingNodes(), this.tasks.loadAdminKey(), this.tasks.prepareUpgradeZip(), this.tasks.checkExistingNodesStakedAmount(), ]; } private updateSubmitTransactionsTasks(): SoloListrTask<NodeUpdateContext>[] { return [ this.tasks.sendNodeUpdateTransaction(), this.tasks.sendPrepareUpgradeTransaction() as SoloListrTask<NodeUpdateContext>, this.tasks.sendFreezeUpgradeTransaction() as SoloListrTask<NodeUpdateContext>, ]; } private updateExecuteTasks(): SoloListrTask<NodeUpdateContext>[] { return [ this.tasks.checkAllNodesAreFrozen('existingNodeAliases'), this.tasks.downloadNodeGeneratedFilesForDynamicAddressBook(), this.tasks.prepareStagingDirectory('allNodeAliases'), this.tasks.copyNodeKeysToSecrets(), this.tasks.getNodeLogsAndConfigs(), this.tasks.updateChartWithConfigMap( 'Update chart to use new configMap due to account number change', NodeSubcommandType.UPDATE, ({config}): boolean => !config.newAccountNumber && !config.debugNodeAlias, ), this.tasks.killNodesAndUpdateConfigMap(), this.tasks.checkNodePodsAreRunning(), this.tasks.fetchPlatformSoftware('allNodeAliases'), this.tasks.setupNetworkNodes('allNodeAliases', false), this.tasks.addWrapsLib(), this.tasks.startNodes('allNodeAliases'), this.tasks.enablePortForwarding(), this.tasks.checkAllNodesAreActive('allNodeAliases'), this.tasks.checkAllNodeProxiesAreActive(), this.tasks.triggerStakeWeightCalculate<NodeUpdateContext>(NodeSubcommandType.UPDATE), this.tasks.finalize(), ]; } private upgradePrepareTasks(argv: ArgvStruct, lease: Lock): SoloListrTask<NodeUpgradeContext>[] { return [ this.tasks.initialize(argv, this.configs.upgradeConfigBuilder.bind(this.configs), lease), this.validateAllNodePhases({excludedPhases: []}), this.tasks.identifyExistingNodes(), this.tasks.loadAdminKey(), this.tasks.prepareUpgradeZip(), this.tasks.checkExistingNodesStakedAmount(), ]; } private upgradeSubmitTransactionsTasks(): SoloListrTask<NodeUpgradeContext>[] { return [ this.tasks.sendPrepareUpgradeTransaction() as SoloListrTask<NodeUpgradeContext>, this.tasks.sendFreezeUpgradeTransaction() as SoloListrTask<NodeUpgradeContext>, ]; } private upgradeExecuteTasks(): SoloListrTask<NodeUpgradeContext>[] { return [ this.tasks.checkAllNodesAreFrozen('existingNodeAliases'), this.tasks.downloadNodeUpgradeFiles(), this.tasks.getNodeLogsAndConfigs(), this.tasks.upgradeNodeConfigurationFilesWithChart(), this.tasks.fetchPlatformSoftware('nodeAliases'), this.tasks.addWrapsLib(), this.tasks.startNodes('allNodeAliases'), this.tasks.enablePortForwarding(), this.tasks.checkAllNodesAreActive('allNodeAliases'), this.tasks.checkAllNodeProxiesAreActive(), this.tasks.finalize(), ]; } /** ******** Handlers **********/ public async prepareUpgrade(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.PREPARE_UPGRADE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.prepareUpgradeConfigBuilder.bind(this.configs), leaseWrapper.lease), this.tasks.identifyExistingNodes(), this.tasks.prepareStagingDirectory('existingNodeAliases'), this.tasks.prepareUpgradeZip(), this.tasks.sendPrepareUpgradeTransaction(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in preparing node upgrade', leaseWrapper.lease, ); return true; } public async freezeUpgrade(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.PREPARE_UPGRADE_FLAGS); await this.commandAction( argv, [ this.tasks.initialize(argv, this.configs.prepareUpgradeConfigBuilder.bind(this.configs)), this.tasks.identifyExistingNodes(), this.tasks.prepareUpgradeZip(), this.tasks.sendFreezeUpgradeTransaction(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in executing node freeze upgrade', ); return true; } public async update(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.updatePrepareTasks(argv, leaseWrapper.lease), ...this.updateSubmitTransactionsTasks(), ...this.updateExecuteTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in updating consensus nodes', leaseWrapper.lease, ); return true; } public async updatePrepare(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_PREPARE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.updatePrepareTasks(argv, leaseWrapper.lease), this.tasks.saveContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, NodeHelper.updateSaveContextParser), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in preparing consensus node update', leaseWrapper.lease, ); return true; } public async updateSubmitTransactions(argv: ArgvStruct): Promise<boolean> { const leaseWrapper: LeaseWrapper = {lease: undefined}; argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_SUBMIT_TRANSACTIONS_FLAGS); await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.updateConfigBuilder.bind(this.configs), leaseWrapper.lease), this.tasks.loadContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, NodeHelper.updateLoadContextParser), ...this.updateSubmitTransactionsTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in submitting transactions for consensus node update', leaseWrapper.lease, ); return true; } public async updateExecute(argv: ArgvStruct): Promise<boolean> { const leaseWrapper: LeaseWrapper = {lease: undefined}; argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_EXECUTE_FLAGS); await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize( argv, this.configs.updateConfigBuilder.bind(this.configs), leaseWrapper.lease, false, ), this.tasks.loadContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, NodeHelper.updateLoadContextParser), ...this.updateExecuteTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in executing network upgrade', leaseWrapper.lease, ); return true; } public async upgradePrepare(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.UPGRADE_PREPARE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.upgradePrepareTasks(argv, leaseWrapper.lease), this.tasks.saveContextData(argv, NodeCommandHandlers.UPGRADE_CONTEXT_FILE, NodeHelper.upgradeSaveContextParser), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in preparing node upgrade', leaseWrapper.lease, ); return true; } public async upgradeSubmitTransactions(argv: ArgvStruct): Promise<boolean> { const leaseWrapper: LeaseWrapper = {lease: undefined}; argv = helpers.addFlagsToArgv(argv, NodeFlags.UPGRADE_SUBMIT_TRANSACTIONS_FLAGS); await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.upgradeConfigBuilder.bind(this.configs), leaseWrapper.lease), this.tasks.loadContextData(argv, NodeCommandHandlers.UPGRADE_CONTEXT_FILE, NodeHelper.upgradeLoadContextParser), ...this.upgradeSubmitTransactionsTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in submitting transactions for node upgrade', leaseWrapper.lease, ); return true; } public async upgradeExecute(argv: ArgvStruct): Promise<boolean> { const leaseWrapper: LeaseWrapper = {lease: undefined}; argv = helpers.addFlagsToArgv(argv, NodeFlags.UPGRADE_FLAGS); await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize( argv, this.configs.upgradeConfigBuilder.bind(this.configs), leaseWrapper.lease, false, ), this.tasks.loadContextData(argv, NodeCommandHandlers.UPGRADE_CONTEXT_FILE, NodeHelper.upgradeLoadContextParser), ...this.upgradeExecuteTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in executing network upgrade', leaseWrapper.lease, ); return true; } public async upgrade(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.UPGRADE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.upgradePrepareTasks(argv, leaseWrapper.lease), ...this.upgradeSubmitTransactionsTasks(), ...this.upgradeExecuteTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in upgrade network', leaseWrapper.lease, ); return true; } public async destroy(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.DESTROY_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.destroyPrepareTaskList(argv, leaseWrapper.lease), ...this.destroySubmitTransactionsTaskList(), ...this.destroyExecuteTaskList(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in destroying nodes', leaseWrapper.lease, ); return true; } public async destroyPrepare(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.DESTROY_PREPARE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.destroyPrepareTaskList(argv, leaseWrapper.lease), this.tasks.saveContextData(argv, NodeCommandHandlers.DESTROY_CONTEXT_FILE, NodeHelper.deleteSaveContextParser), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in preparing to destroy a node', leaseWrapper.lease, ); return true; } public async destroySubmitTransactions(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.DESTROY_SUBMIT_TRANSACTIONS_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.destroyConfigBuilder.bind(this.configs), leaseWrapper.lease), this.tasks.loadContextData(argv, NodeCommandHandlers.DESTROY_CONTEXT_FILE, NodeHelper.deleteLoadContextParser), ...this.destroySubmitTransactionsTaskList(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in deleting a node', leaseWrapper.lease, ); return true; } public async destroyExecute(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.DESTROY_EXECUTE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.destroyConfigBuilder.bind(this.configs), leaseWrapper.lease, false), this.tasks.loadContextData(argv, NodeCommandHandlers.DESTROY_CONTEXT_FILE, NodeHelper.deleteLoadContextParser), ...this.destroyExecuteTaskList(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in deleting a node', leaseWrapper.lease, ); return true; } public async add(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.addPrepareTasks(argv, leaseWrapper.lease), ...this.addSubmitTransactionsTasks(), ...this.addExecuteTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in adding consensus node', leaseWrapper.lease, ); return true; } public async addPrepare(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_PREPARE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), ...this.addPrepareTasks(argv, leaseWrapper.lease), this.tasks.saveContextData(argv, NodeCommandHandlers.ADD_CONTEXT_FILE, helpers.addSaveContextParser), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in preparing node', leaseWrapper.lease, ); return true; } public async addSubmitTransactions(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_SUBMIT_TRANSACTIONS_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.addConfigBuilder.bind(this.configs), leaseWrapper.lease), this.tasks.loadContextData(argv, NodeCommandHandlers.ADD_CONTEXT_FILE, helpers.addLoadContextParser), ...this.addSubmitTransactionsTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, '`Error in submitting transactions to node', leaseWrapper.lease, ); return true; } public async addExecute(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_EXECUTE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize( argv, this.configs.addConfigBuilder.bind(this.configs), leaseWrapper.lease, false, ), this.tasks.identifyExistingNodes(), this.tasks.loadContextData(argv, NodeCommandHandlers.ADD_CONTEXT_FILE, helpers.addLoadContextParser), ...this.addExecuteTasks(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in adding node', leaseWrapper.lease, ); return true; } public async logs(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.LOGS_FLAGS); if (!argv[flags.deployment.name]) { argv[flags.deployment.name] = await this.resolveDeploymentForLogs(argv); } const outputDirectory: string = this.resolveOutputDirectory(argv); await this.commandAction( argv, [ this.tasks.initialize(argv, this.configs.logsConfigBuilder.bind(this.configs), null, true, false), this.tasks.getNodeLogsAndConfigs(undefined, outputDirectory), this.tasks.getHelmChartValues(outputDirectory), GetSoloRemoteConfigMapTask.getTask(this.k8Factory, this.logger, outputDirectory), this.tasks.downloadHieroComponentLogs(outputDirectory), this.tasks.analyzeCollectedDiagnostics(outputDirectory), this.tasks.reportActivePortForwards(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in downloading logs from nodes', ); this.logger.showUser( chalk.yellow( '\n⚠ Warning: Collected diagnostic data contains sensitive node configuration\n' + ' (TLS certificates, private keys, onboard data). Store it securely and do\n' + ' not share publicly without reviewing the contents first.', ), ); return true; } public async analyze(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.ANALYZE_FLAGS); this.nodeConfigManager.update(argv); const inputDirectory: string = this.nodeConfigManager.getFlag<string>(flags.inputDir) || ''; await this.commandAction( argv, [this.tasks.analyzeCollectedDiagnostics(inputDirectory)], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error analyzing diagnostics logs', ); return true; } private async resolveDeploymentForLogs(argv: ArgvStruct): Promise<string> { const deploymentFromFlag: string = this.resolveDeploymentFlag(argv); if (deploymentFromFlag && deploymentFromFlag.trim()) { return deploymentFromFlag; } await this.localConfig.load(); const deployments: MutableFacadeArray<Deployment, DeploymentSchema> = this.localConfig.configuration.deployments; const validDeployments: Deployment[] = []; for (const deployment of deployments) { if (deployment?.name && deployment.name.trim().length > 0) { validDeployments.push(deployment); } } if (validDeployments.length === 0) { const remoteDeployments: Map<string, RemoteDeploymentInfo> = await findDeploymentsFromRemoteConfig( this.k8Factory, this.logger, ); if (remoteDeployments.size === 0) { throw new SoloError( `No deployments found in local or remote config. Please provide --${flags.deployment.name} or create a deployment first.`, ); } const remoteDeploymentNames: string[] = [...remoteDeployments.keys()]; if (remoteDeploymentNames.length === 1) { const selectedFromRemote: string = remoteDeploymentNames[0]; this.logger.showUser(`Using deployment from remote config: ${selectedFromRemote}`); return selectedFromRemote; } if (this.resolveQuietFlag(argv)) { const names: string = remoteDeploymentNames.join(', '); throw new SoloError( `Multiple deployments found in remote config (${names}). Please provide --${flags.deployment.name}.`, ); } this.ensureInteractiveSelectionPrompt(); const selectedFromRemote: string = (await selectPrompt({ message: 'Select deployment for diagnostics logs:', choices: remoteDeploymentNames.map((name: string) => ({name, value: name})), })) as string; this.logger.showUser(`Using selected deployment: ${selectedFromRemote}`); return selectedFromRemote; } if (validDeployments.length === 1) { const deploymentName: string = validDeployments[0].name; this.logger.showUser(`Using deployment from local config: ${deploymentName}`); return deploymentName; } if (this.resolveQuietFlag(argv)) { const deploymentNames: string = validDeployments.map((deployment: Deployment) => deployment.name).join(', '); throw new SoloError( `Multiple deployments found in local config (${deploymentNames}). Please provide --${flags.deployment.name}.`, ); } this.ensureInteractiveSelectionPrompt(); const selectedDeployment: string = (await selectPrompt({ message: 'Select deployment for diagnostics logs:', choices: validDeployments.map((deployment): {name: string; value: string} => ({ name: deployment.name, value: deployment.name, })), })) as string; this.logger.showUser(`Using selected deployment: ${selectedDeployment}`); return selectedDeployment; } public async all(argv: ArgvStruct, excludeSensitiveData: boolean = false): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.DIAGNOSTICS_CONNECTIONS); if (!argv[flags.deployment.name]) { argv[flags.deployment.name] = await this.resolveDeploymentForLogs(argv); } const outputDirectory: string = this.resolveOutputDirectory(argv); await this.commandAction( argv, [ this.tasks.initialize(argv, this.configs.logsConfigBuilder.bind(this.configs), null, true, false), this.tasks.getNodeLogsAndConfigs(excludeSensitiveData, outputDirectory), ...(excludeSensitiveData ? [] : [this.tasks.getHelmChartValues(outputDirectory)]), GetSoloRemoteConfigMapTask.getTask(this.k8Factory, this.logger, outputDirectory), this.tasks.downloadHieroComponentLogs(outputDirectory), this.tasks.analyzeCollectedDiagnostics(outputDirectory), // do not call validateConnectionsTaskList since node could be stopped or not active but logs are still needed ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in diagnosing deployment', ); return true; } public async debug(argv: ArgvStruct, excludeSensitiveData: boolean = false): Promise<boolean> { // First run all diagnostics await this.all(argv, excludeSensitiveData); // Then create a zip file from the logs directory const outputDirectory: string = this.resolveOutputDirectory(argv, constants.SOLO_LOGS_DIR); const deployment: string = this.resolveDeploymentFlag(argv); const timestamp: string = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-').slice(0, 19); const zipFileName: string = `solo-debug-${deployment}-${timestamp}.zip`; const zipFilePath: string = PathEx.join(outputDirectory, '..', zipFileName); this.logger.showUser(chalk.cyan(`\nCreating debug archive from: ${outputDirectory}`)); this.logger.showUser(chalk.cyan(`Archive location: ${zipFilePath}`)); if (!excludeSensitiveData) { this.logger.showUser( chalk.yellow( '\n⚠ Warning: The debug archive contains sensitive node configuration\n' + ' (TLS certificates, private keys, onboard data). Review its contents\n' + ' before sharing. Private keys under data/keys are NOT excluded.', ), ); } try { await this.zippy.zip(outputDirectory, zipFilePath); this.logger.showUser(chalk.green('✓ Debug information collected successfully!')); this.logger.showUser(chalk.cyan(` Archive: ${zipFilePath}`)); } catch (error: Error | unknown) { throw new SoloError(`Failed to create debug archive: ${(error as Error).message}`, error as Error); } return true; } public async connections(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.DIAGNOSTICS_CONNECTIONS); await this.commandAction( argv, [ this.tasks.initialize(argv, this.configs.connectionsConfigBuilder.bind(this.configs)), ...this.validateConnectionsTaskList(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in testing connections to components', ); return true; } private validateConnectionsTaskList(): SoloListrTask<AnyListrContext>[] { return [ this.tasks.prepareDiagnosticsData(), this.tasks.validateLocalPorts(), this.tasks.testAccountCreation(), this.tasks.fetchAccountFromExplorer(), this.tasks.testRelay(), ]; } /** * Collects a full debug archive for the deployment (logs + configs + zip) and * then creates a GitHub issue using the `gh` CLI with a pre-filled title and body. * The generated archive is referenced for the user to attach manually via the GitHub UI. * */ public async report(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.REPORT_FLAGS); // Resolve deployment before calling collectDebug() so it's available for the issue title/body const deployment: string = argv[flags.deployment.name] ? String(argv[flags.deployment.name]) : await this.resolveDeploymentForLogs(argv); if (!argv[flags.deployment.name]) { argv[flags.deployment.name] = deployment; } await DiagnosticsReporter.runDiagnosticsReport({ logger: this.logger, deployment, outputDirectory: this.resolveOutputDirectory(argv, constants.SOLO_LOGS_DIR), soloVersion: getSoloVersion(), isQuiet: this.resolveQuietFlag(argv), collectDebug: async (): Promise<void> => { await this.debug(argv, true); }, }); return true; } public async states(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.STATES_FLAGS); await this.commandAction( argv, [ this.tasks.initialize(argv, this.configs.statesConfigBuilder.bind(this.configs)), this.tasks.getNodeStateFiles(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in downloading states from nodes', ); return true; } public async refresh(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.REFRESH_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.refreshConfigBuilder.bind(this.configs), leaseWrapper.lease), this.validateAllNodePhases({ acceptedPhases: [DeploymentPhase.STARTED, DeploymentPhase.CONFIGURED, DeploymentPhase.DEPLOYED], }), this.tasks.identifyNetworkPods(), this.tasks.dumpNetworkNodesSaveState(), this.tasks.downloadLastState(), this.tasks.uploadStateToNewNode(), this.tasks.fetchPlatformSoftware('nodeAliases'), this.tasks.setupNetworkNodes('nodeAliases', true), this.tasks.startNodes('nodeAliases'), this.tasks.checkNodesAndProxiesAreActive('nodeAliases'), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in refreshing nodes', leaseWrapper.lease, ); return true; } public async keys(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.KEYS_FLAGS); await this.commandAction( argv, [ this.tasks.initialize(argv, this.configs.keysConfigBuilder.bind(this.configs)), this.tasks.generateGossipKeys(), this.tasks.generateGrpcTlsKeys(), this.tasks.finalize(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error generating keys', undefined, 'keys consensus generate', ); return true; } public async stop(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.STOP_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.stopConfigBuilder.bind(this.configs), leaseWrapper.lease), this.validateAllNodePhases({ acceptedPhases: [DeploymentPhase.STARTED, DeploymentPhase.CONFIGURED], }), this.tasks.identifyNetworkPods(1), this.tasks.stopNodes('nodeAliases'), this.changeAllNodePhases(DeploymentPhase.STOPPED), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error stopping node', leaseWrapper.lease, ); return true; } public async start(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.START_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize( argv, this.configs.startConfigBuilder.bind(this.configs), leaseWrapper.lease, true, false, ), this.validateAllNodePhases({acceptedPhases: [DeploymentPhase.CONFIGURED]}), this.tasks.identifyExistingNodes(), this.tasks.uploadStateFiles(({config}): boolean => config.stateFile.length === 0), this.tasks.startNodes('nodeAliases'), this.tasks.enablePortForwarding(true), this.tasks.checkNodesAndProxiesAreActive('nodeAliases'), this.tasks.waitForTss(), this.tasks.setGrpcWebEndpoint('nodeAliases', NodeSubcommandType.START), this.changeAllNodePhases(DeploymentPhase.STARTED, LedgerPhase.INITIALIZED), this.tasks.addNodeStakes(), this.tasks.emitNodeStartedEvent(), // TODO only show this if we are not running in one-shot mode // this.tasks.showUserMessages(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error starting node', leaseWrapper.lease, 'consensus node start', ); return true; } public async setup(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.SETUP_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize(argv, this.configs.setupConfigBuilder.bind(this.configs), leaseWrapper.lease), this.validateAllNodePhases({ acceptedPhases: [DeploymentPhase.DEPLOYED], }), this.tasks.identifyNetworkPods(), this.tasks.fetchPlatformSoftware('nodeAliases'), this.tasks.setupNetworkNodes('nodeAliases', true), this.tasks.setupNetworkNodeFolders(), this.changeAllNodePhases(DeploymentPhase.CONFIGURED), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error in setting up nodes', leaseWrapper.lease, 'consensus node setup', ); return true; } public async freeze(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.FREEZE_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager, false), this.tasks.initialize( argv, this.configs.freezeConfigBuilder.bind(this.configs), leaseWrapper.lease, true, false, ), this.tasks.identifyExistingNodes(), this.tasks.sendFreezeTransaction(), this.tasks.checkAllNodesAreFrozen('existingNodeAliases'), this.tasks.stopNodes('existingNodeAliases'), this.changeAllNodePhases(DeploymentPhase.FROZEN), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error freezing node', leaseWrapper.lease, ); return true; } public async restart(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.RESTART_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager), this.tasks.initialize( argv, this.configs.restartConfigBuilder.bind(this.configs), leaseWrapper.lease, true, false, ), this.tasks.identifyExistingNodes(), this.tasks.addWrapsLib(), this.tasks.startNodes('existingNodeAliases'), this.tasks.enablePortForwarding(), this.tasks.checkNodesAndProxiesAreActive('existingNodeAliases'), this.changeAllNodePhases(DeploymentPhase.STARTED), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, 'Error restarting node', leaseWrapper.lease, ); return true; } /** * Changes the state from all consensus nodes components in remote config. * * @param phase - to which to change the consensus node component * @param ledgerPhase */ public changeAllNodePhases( phase: DeploymentPhase, ledgerPhase: Optional<LedgerPhase> = undefined, ): SoloListrTask<AnyListrContext> { interface Context { config: {namespace: NamespaceName; consensusNodes: ConsensusNode[]}; } return { title: `Change node state to ${phase} in remote config`, skip: (): boolean => !this.remoteConfig.isLoaded(), task: async (context_: Context): Promise<void> => { for (const consensusNode of context_.config.consensusNodes) { const componentId: ComponentId = Templates.renderComponentIdFromNodeAlias(consensusNode.name); this.remoteConfig.configuration.components.changeNodePhase(componentId, phase); } if (ledgerPhase) { this.remoteConfig.configuration.state.ledgerPhase = ledgerPhase; } await this.remoteConfig.persist(); }, }; } /** * Creates tasks to validate that each node state is either one of the accepted states or not one of the excluded. * * @param acceptedPhases - the state at which the nodes can be, not matching any of the states throws an error * @param excludedPhases - the state at which the nodes can't be, matching any of the states throws an error */ public validateAllNodePhases({ acceptedPhases, excludedPhases, }: { acceptedPhases?: DeploymentPhase[]; excludedPhases?: DeploymentPhase[]; }): SoloListrTask<AnyListrContext> { interface Context { config: {namespace: string; nodeAliases: NodeAliases}; } return { title: 'Validate nodes states', skip: (): boolean => !this.remoteConfig.isLoaded(), task: (context_: Context, task): SoloListr<Context> => { const nodeAliases: NodeAliases = context_.config.nodeAliases; const subTasks: SoloListrTask<Context>[] = nodeAliases.map( (nodeAlias): SoloListrTask<AnyListrContext> => ({ title: `Validating state for node ${nodeAlias}`, task: (_, task): void => { const state: DeploymentPhase = this.validateNodeState( nodeAlias, this.remoteConfig.configuration.components, acceptedPhases, excludedPhases, ); task.title += ` - ${chalk.green('valid state')}: ${chalk.cyan(state)}`; }, }), ); return task.newListr(subTasks, { concurrent: false, rendererOptions: {collapseSubtasks: false}, }); }, }; } /** * Creates tasks to validate that specific node state is either one of the accepted states or not one of the excluded. * * @param acceptedPhases - the state at which the node can be, not matching any of the states throws an error * @param excludedPhases - the state at which the node can't be, matching any of the states throws an error */ public validateSingleNodeState({ acceptedPhases, excludedPhases, }: { acceptedPhases?: DeploymentPhase[]; excludedPhases?: DeploymentPhase[]; }): SoloListrTask<AnyListrContext> { void acceptedPhases; void excludedPhases; interface Context { config: {namespace: string; nodeAlias: NodeAlias}; } return { title: 'Validate nodes state', skip: (): boolean => !this.remoteConfig.isLoaded(), task: (context_: Context, task): void => { const nodeAlias: NodeAlias = context_.config.nodeAlias; task.title += ` ${nodeAlias}`; // TODO: Disabled for now until the node's state mapping is completed // const components = this.remoteConfig.components; // const state = this.validateNodeState(nodeAlias, components, acceptedPhases, excludedPhases); // task.title += ` - ${chalk.green('valid state')}: ${chalk.cyan(state)}`; }, }; } /** * @param nodeAlias - the alias of the node whose state to validate * @param components - the component data wrapper * @param acceptedPhases - the state at which the node can be, not matching any of the states throws an error * @param excludedPhases - the state at which the node can't be, matching any of the states throws an error */ private validateNodeState( nodeAlias: NodeAlias, components: ComponentsDataWrapperApi, acceptedPhases: Optional<DeploymentPhase[]>, excludedPhases: Optional<DeploymentPhase[]>, ): DeploymentPhase { void acceptedPhases; void excludedPhases; let nodeComponent: ConsensusNodeStateSchema; try { nodeComponent = components.getComponent<ConsensusNodeStateSchema>( ComponentTypes.ConsensusNode, Templates.renderComponentIdFromNodeAlias(nodeAlias), ); } catch { throw new SoloError(`${nodeAlias} not found in remote config`); } // TODO: Enable once states have been mapped // if (acceptedPhases && !acceptedPhases.includes(nodeComponent.state)) { // const errorMessageData = // `accepted states: ${acceptedPhases.join(', ')}, ` + `current state: ${nodeComponent.state}`; // throw new SoloError(`${nodeAlias} has invalid state - ` + errorMessageData); // } // // if (excludedPhases && excludedPhases.includes(nodeComponent.state)) { // const errorMessageData = // `excluded states: ${excludedPhases.join(', ')}, ` + `current state: ${nodeComponent.state}`; // throw new SoloError(`${nodeAlias} has invalid state - ` + errorMessageData); // } return nodeComponent.metadata.phase; } public async collectJavaFlightRecorderLogs(argv: ArgvStruct): Promise<boolean> { argv = helpers.addFlagsToArgv(argv, NodeFlags.COLLECT_JFR_FLAGS); const leaseWrapper: LeaseWrapper = {lease: undefined}; await this.commandAction( argv, [ this.tasks.loadConfiguration(argv, leaseWrapper, this.leaseManager