UNPKG

@hashgraph/solo

Version:

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

1,029 lines (952 loc) 88.5 kB
// SPDX-License-Identifier: Apache-2.0 import {Listr, ListrRendererValue} from 'listr2'; import {SoloError} from '../../core/errors/solo-error.js'; import * as constants from '../../core/constants.js'; import {BaseCommand} from '../base.js'; import {Flags as flags, Flags} from '../flags.js'; import { type AnyListrContext, type AnyObject, type ArgvStruct, type NodeAlias, type NodeId, } from '../../types/aliases.js'; import { type DeploymentName, type Optional, type Realm, type Shard, type SoloListr, type SoloListrTask, type SoloListrTaskWrapper, } from '../../types/index.js'; import {type CommandFlag, type CommandFlags} from '../../types/flag-types.js'; import {inject, injectable} from 'tsyringe-neo'; import {NamespaceName} from '../../types/namespace/namespace-name.js'; import {StringEx} from '../../business/utils/string-ex.js'; import {OneShotCommand} from './one-shot.js'; import {OneShotSingleDeployConfigClass, OneShotVersionsObject} from './one-shot-single-deploy-config-class.js'; import {OneShotSingleDeployContext} from './one-shot-single-deploy-context.js'; import {OneShotSingleDestroyConfigClass} from './one-shot-single-destroy-config-class.js'; import * as version from '../../../version.js'; import {confirm as confirmPrompt, select as selectPrompt} from '@inquirer/prompts'; import {ClusterReferenceCommandDefinition} from '../command-definitions/cluster-reference-command-definition.js'; import {DeploymentCommandDefinition} from '../command-definitions/deployment-command-definition.js'; import {ConsensusCommandDefinition} from '../command-definitions/consensus-command-definition.js'; import {KeysCommandDefinition} from '../command-definitions/keys-command-definition.js'; import {MirrorCommandDefinition} from '../command-definitions/mirror-command-definition.js'; import {ExplorerCommandDefinition} from '../command-definitions/explorer-command-definition.js'; import {RelayCommandDefinition} from '../command-definitions/relay-command-definition.js'; import {patchInject} from '../../core/dependency-injection/container-helper.js'; import {InjectTokens} from '../../core/dependency-injection/inject-tokens.js'; import {type AccountManager} from '../../core/account-manager.js'; import { CreatedPredefinedAccount, PREDEFINED_ACCOUNT_GROUPS, PredefinedAccount, predefinedEcdsaAccountsWithAlias, SystemAccount, } from './predefined-accounts.js'; import { AccountId, Client, HbarUnit, PublicKey, TopicCreateTransaction, TopicId, TopicInfoQuery, } from '@hiero-ledger/sdk'; import * as helpers from '../../core/helpers.js'; import {createDirectoryIfNotExists, entityId, remoteConfigsToDeploymentsTable} from '../../core/helpers.js'; import {Duration} from '../../core/time/duration.js'; import {resolveNamespaceFromDeployment} from '../../core/resolvers.js'; import fs from 'node:fs'; import path from 'node:path'; import chalk from 'chalk'; import {PathEx} from '../../business/utils/path-ex.js'; import yaml from 'yaml'; import {BlockCommandDefinition} from '../command-definitions/block-command-definition.js'; import {argvPushGlobalFlags, invokeSoloCommand, newArgv, optionFromFlag} from '../command-helpers.js'; import {ConfigMap} from '../../integration/kube/resources/config-map/config-map.js'; import {type K8} from '../../integration/kube/k8.js'; import {Templates} from '../../core/templates.js'; import {ListrInquirerPromptAdapter} from '@listr2/prompt-adapter-inquirer'; import {SemanticVersion} from '../../business/utils/semantic-version.js'; import {type Lock} from '../../core/lock/lock.js'; import {ListrLock} from '../../core/lock/listr-lock.js'; import {ResourceNotFoundError} from '../../integration/kube/errors/resource-operation-errors.js'; import {NoKubeConfigContextError} from '../../business/runtime-state/errors/no-kube-config-context-error.js'; import {RelayNodeStateSchema} from '../../data/schema/model/remote/state/relay-node-state-schema.js'; import {DeploymentPhase} from '../../data/schema/model/remote/deployment-phase.js'; import {ComponentTypes} from '../../core/config/remote/enumerations/component-types.js'; import {MirrorNodeStateSchema} from '../../data/schema/model/remote/state/mirror-node-state-schema.js'; import {ExplorerStateSchema} from '../../data/schema/model/remote/state/explorer-state-schema.js'; import {BlockNodeStateSchema} from '../../data/schema/model/remote/state/block-node-state-schema.js'; import {type SoloEventBus} from '../../core/events/solo-event-bus.js'; import {SoloEventType} from '../../core/events/event-types/solo-event.js'; import {MirrorNodeDeployedEvent} from '../../core/events/event-types/mirror-node-deployed-event.js'; import {NodesStartedEvent} from '../../core/events/event-types/nodes-started-event.js'; import {DeploymentSchema} from '../../data/schema/model/local/deployment-schema.js'; import {Deployment} from '../../business/runtime-state/config/local/deployment.js'; import {MutableFacadeArray} from '../../business/runtime-state/collection/mutable-facade-array.js'; import {StringFacade} from '../../business/runtime-state/facade/string-facade.js'; import {DeploymentStateSchema} from '../../data/schema/model/remote/deployment-state-schema.js'; import {OneShotInfoContext} from './one-shot-info-context.js'; import {ApplicationVersionsSchema} from '../../data/schema/model/common/application-versions-schema.js'; import {CacheCommandDefinition} from '../command-definitions/cache-command-definition.js'; @injectable() export class DefaultOneShotCommand extends BaseCommand implements OneShotCommand { private static readonly SINGLE_DEPLOY_CONFIGS_NAME: string = 'singleAddConfigs'; private static readonly SINGLE_DESTROY_CONFIGS_NAME: string = 'singleDestroyConfigs'; private _isRollback: boolean = false; public static readonly DEPLOY_FLAGS_LIST: CommandFlags = { required: [], optional: [ flags.quiet, flags.force, flags.deployment, flags.namespace, flags.clusterRef, flags.minimalSetup, flags.rollback, flags.parallelDeploy, flags.externalAddress, flags.edgeEnabled, ], }; public static readonly MULTI_DEPLOY_FLAGS_LIST: CommandFlags = { required: [], optional: [...DefaultOneShotCommand.DEPLOY_FLAGS_LIST.optional, flags.numberOfConsensusNodes], }; public static readonly DESTROY_FLAGS_LIST: CommandFlags = { required: [], optional: [flags.quiet, flags.deployment], }; public static readonly FALCON_DEPLOY_FLAGS_LIST: CommandFlags = { required: [], optional: [ flags.quiet, flags.force, flags.valuesFile, flags.numberOfConsensusNodes, flags.deployment, flags.namespace, flags.clusterRef, flags.deployMirrorNode, flags.deployExplorer, flags.deployRelay, flags.deployMetricsServer, flags.rollback, flags.parallelDeploy, flags.externalAddress, ], }; public static readonly FALCON_DESTROY_FLAGS_LIST: CommandFlags = { required: [], optional: [...DefaultOneShotCommand.DESTROY_FLAGS_LIST.optional], }; public static readonly INFO_FLAGS_LIST: CommandFlags = { required: [], optional: [flags.quiet, flags.deployment], }; public constructor( @inject(InjectTokens.AccountManager) private readonly accountManager: AccountManager, @inject(InjectTokens.SoloEventBus) private readonly eventBus: SoloEventBus, ) { super(); this.accountManager = patchInject(accountManager, InjectTokens.AccountManager, this.constructor.name); this.eventBus = patchInject(eventBus, InjectTokens.SoloEventBus, this.constructor.name); } /** * Concatenates a default config file with an override file, writing the result to outputFilePath. * Later entries in the file override earlier ones, so the override values take precedence. */ private concatConfigFiles(defaultFilePath: string, overrideFilePath: string, outputFilePath: string): string { const defaultContent: string = fs.existsSync(defaultFilePath) ? fs.readFileSync(defaultFilePath, 'utf8') : ''; const overrideContent: string = fs.existsSync(overrideFilePath) ? fs.readFileSync(overrideFilePath, 'utf8') : ''; const outputDirectory: string = path.dirname(outputFilePath); if (!fs.existsSync(outputDirectory)) { fs.mkdirSync(outputDirectory, {recursive: true}); } fs.writeFileSync(outputFilePath, defaultContent.trimEnd() + '\n' + overrideContent); return outputFilePath; } /** * Appends non-empty config entries to the argv array as CLI flags. * @param argv - The argument array to append to * @param configSection - The config object to extract key-value pairs from */ private appendConfigToArgv(argv: string[], configSection: AnyObject): void { if (!configSection) { return; } for (const [key, value] of Object.entries(configSection)) { if ( value !== undefined && value !== null && value !== StringEx.EMPTY && key !== flags.getFormattedFlagKey(Flags.deployment) ) { argv.push(`${key}`, value.toString()); } } } public async deploy(argv: ArgvStruct): Promise<boolean> { return this.deployInternal(argv, DefaultOneShotCommand.DEPLOY_FLAGS_LIST); } public async deployFalcon(argv: ArgvStruct): Promise<boolean> { return this.deployInternal(argv, DefaultOneShotCommand.FALCON_DEPLOY_FLAGS_LIST); } private async performRollback( deployError: Error, config: OneShotSingleDeployConfigClass | undefined, ): Promise<never> { if (!config) { throw new SoloError( `Deploy failed: ${deployError.message}. Rollback skipped: no resources created.`, deployError, ); } if (config.rollback === false) { this.logger.warn('Automatic rollback skipped (--no-rollback flag provided)'); this.logger.warn('To clean up: solo one-shot single destroy'); this.logger.warn(`Or: kubectl delete ns ${config.namespace.name}`); throw new SoloError(`Deploy failed: ${deployError.message}. Rollback skipped (--no-rollback).`, deployError); } this.logger.warn( `Deploy failed. Starting automatic rollback for deployment '${config.deployment}' in namespace '${config.namespace.name}'...`, ); const destroyArgv: ArgvStruct = { _: [], deployment: config.deployment, clusterRef: config.clusterRef, namespace: config.namespace.name, context: config.context, quiet: true, }; this._isRollback = true; try { await this.destroyInternal(destroyArgv, DefaultOneShotCommand.DESTROY_FLAGS_LIST); } catch (rollbackError) { this.logger.error(`Rollback failed for deployment '${config.deployment}': ${rollbackError.message}`); throw new SoloError( `Deploy failed: ${deployError.message}. Rollback also failed: ${rollbackError.message}`, deployError, ); } finally { // Safety net: ensure namespace is always deleted during rollback, even if destroyInternal // failed or skipped namespace cleanup (e.g. due to skipAll, helm uninstall failure, etc.) try { const k8: K8 = this.k8Factory.getK8(config.context); if (await k8.namespaces().has(config.namespace)) { this.logger.warn(`Rollback cleanup: deleting namespace '${config.namespace.name}'`); await k8.namespaces().delete(config.namespace); } } catch (cleanupError) { this.logger.warn( `Failed to delete namespace '${config.namespace.name}' during rollback cleanup: ${cleanupError.message}`, ); } this._isRollback = false; } this.logger.info(`Rollback complete. Cache preserved at: ${config.cacheDir}`); throw new SoloError(`Deploy failed: ${deployError.message}. Rollback completed successfully.`, deployError); } private async deployInternal(argv: ArgvStruct, flagsList: CommandFlags): Promise<boolean> { let config: OneShotSingleDeployConfigClass | undefined = undefined; let oneShotLease: Lock | undefined; const mirrorNodeId: number = 1; const tasks: Listr<OneShotSingleDeployContext, ListrRendererValue, ListrRendererValue> = this.taskList.newOneShotSingleDeployTaskList( [ { title: 'Initialize', task: async ( context_: OneShotSingleDeployContext, task: SoloListrTaskWrapper<OneShotSingleDeployContext>, ): Promise<void> => { this.configManager.update(argv); this.oneShotState.activate(); const edgeEnabled: boolean = this.configManager.getFlag(Flags.edgeEnabled); const versions: OneShotVersionsObject = this.resolveOneShotComponentVersions(edgeEnabled); // Pre-set component version flags in configManager so they are available // for all sub-commands during concurrent execution this.configManager.setFlag(Flags.releaseTag, versions.consensus); this.configManager.setFlag(Flags.blockNodeChartVersion, versions.blockNode); this.configManager.setFlag(Flags.mirrorNodeVersion, versions.mirror); this.configManager.setFlag(Flags.relayReleaseTag, versions.relay); this.configManager.setFlag(Flags.explorerVersion, versions.explorer); this.configManager.setFlag(Flags.soloChartVersion, versions.soloChart); flags.disablePrompts(flagsList.optional); const allFlags: CommandFlag[] = [...flagsList.required, ...flagsList.optional]; await this.configManager.executePrompt(task, allFlags); context_.config = this.configManager.getConfig( DefaultOneShotCommand.SINGLE_DEPLOY_CONFIGS_NAME, allFlags, ) as OneShotSingleDeployConfigClass; config = context_.config; // Initialize component config sections to empty objects to prevent undefined errors config.consensusNodeConfiguration = {}; config.mirrorNodeConfiguration = {}; config.blockNodeConfiguration = {}; config.explorerNodeConfiguration = {}; config.relayNodeConfiguration = {}; config.networkConfiguration = {}; config.setupConfiguration = {}; config.versions = versions; config.cacheDir ??= constants.SOLO_CACHE_DIR; // if valuesFile is set, read the yaml file and save flags to different config sections to be used // later for consensus node, mirror node, block node, explorer node, relay node if (config.valuesFile) { const valuesFileContent: string = fs.readFileSync(context_.config.valuesFile, 'utf8'); const profileItems: Record<string, AnyObject> = yaml.parse(valuesFileContent) as Record< string, AnyObject >; // Override with values from file if they exist if (profileItems.network) { config.networkConfiguration = profileItems.network; } if (profileItems.setup) { config.setupConfiguration = profileItems.setup; } if (profileItems.consensusNode) { config.consensusNodeConfiguration = profileItems.consensusNode; } if (profileItems.mirrorNode) { config.mirrorNodeConfiguration = profileItems.mirrorNode; } if (profileItems.blockNode) { config.blockNodeConfiguration = profileItems.blockNode; } if (profileItems.explorerNode) { config.explorerNodeConfiguration = profileItems.explorerNode; } if (profileItems.relayNode) { config.relayNodeConfiguration = profileItems.relayNode; } } config.clusterRef = config.clusterRef || 'one-shot'; config.context = config.context || this.k8Factory.default().contexts().readCurrent(); config.deployment = config.deployment || 'one-shot'; config.namespace = config.namespace || NamespaceName.of('one-shot'); this.configManager.setFlag(flags.namespace, config.namespace); config.numberOfConsensusNodes = config.numberOfConsensusNodes || 1; config.force = argv.force; // Ensure release tag is set in network configuration so subcommands use the correct version const releaseTagKey: string = flags.getFormattedFlagKey(Flags.releaseTag); if (!config.networkConfiguration[releaseTagKey]) { config.networkConfiguration[releaseTagKey] = versions.consensus; } if (!config.setupConfiguration[releaseTagKey]) { config.setupConfiguration[releaseTagKey] = versions.consensus; } this.logger.addLogBindings({ clusterReference: config.clusterRef, context: config.context, deployment: config.deployment, namespace: config.namespace.name, }); // Apply small-memory node configuration only for CN >= 0.72.0 and when not using `one-shot falcon deploy` const MINIMUM_CN_VERSION_FOR_SMALL_MEMORY: string = 'v0.72.0-0'; const MINIMUM_CN_VERSION_FOR_STATE_ON_DISK: string = 'v0.73.0-0'; const cnVersion: SemanticVersion<string> = new SemanticVersion(versions.consensus); if (!config.valuesFile && cnVersion.greaterThanOrEqual(MINIMUM_CN_VERSION_FOR_SMALL_MEMORY)) { const defaultsDirectory: string = PathEx.join(constants.SOLO_CACHE_DIR, 'templates'); const overridesDirectory: string = PathEx.join(defaultsDirectory, 'small-memory'); const stateOnDiskDirectory: string = PathEx.join(defaultsDirectory, 'small-memory-state-on-disk'); const mergedDirectory: string = PathEx.join(defaultsDirectory, 'small-memory-merged'); const settingsOverrideFile: string = config.numberOfConsensusNodes > 1 ? 'settings-multinode.txt' : 'settings-single.txt'; const useStateOnDisk: boolean = cnVersion.greaterThanOrEqual(MINIMUM_CN_VERSION_FOR_STATE_ON_DISK); const settingsMergedPath: string = PathEx.join(mergedDirectory, 'settings.txt'); // Merge default settings with small-memory overrides this.concatConfigFiles( PathEx.join(defaultsDirectory, 'settings.txt'), PathEx.join(overridesDirectory, settingsOverrideFile), settingsMergedPath, ); // For CN >= 0.73.0, append state-on-disk settings config.networkConfiguration[flags.getFormattedFlagKey(flags.settingTxt)] = useStateOnDisk ? this.concatConfigFiles( settingsMergedPath, PathEx.join(stateOnDiskDirectory, 'settings.txt'), settingsMergedPath, ) : settingsMergedPath; config.networkConfiguration[flags.getFormattedFlagKey(flags.applicationProperties)] = this.concatConfigFiles( PathEx.join(defaultsDirectory, constants.APPLICATION_PROPERTIES), PathEx.join(overridesDirectory, constants.APPLICATION_PROPERTIES), PathEx.join(mergedDirectory, constants.APPLICATION_PROPERTIES), ); // For CN >= 0.73.0, use state-on-disk application.env instead of default small-memory config.networkConfiguration[flags.getFormattedFlagKey(flags.applicationEnv)] = useStateOnDisk ? PathEx.join(stateOnDiskDirectory, 'application.env') : PathEx.join(overridesDirectory, 'application.env'); const throttlesFile: string = PathEx.join(overridesDirectory, 'throttles.json'); if (fs.existsSync(throttlesFile)) { config.networkConfiguration[flags.getFormattedFlagKey(flags.genesisThrottlesFile)] = throttlesFile; } // For CN >= 0.73.0, cap K8s container memory at 1Gi to prevent unbounded mmap'd state-on-disk page cache growth if (useStateOnDisk) { const helmOverrideFile: string = PathEx.join(stateOnDiskDirectory, 'helm-overrides.yaml'); if (fs.existsSync(helmOverrideFile)) { config.networkConfiguration[flags.getFormattedFlagKey(flags.valuesFile)] = `${config.clusterRef}=${helmOverrideFile}`; } } } // Auto-enable PVCs in network configuration when --local-build-path is used in setup configuration. // Node PVCs are required to persist custom JARs across pod restarts. if ( config.setupConfiguration[flags.getFormattedFlagKey(flags.localBuildPath)] && !config.networkConfiguration[flags.getFormattedFlagKey(flags.persistentVolumeClaims)] ) { this.logger.info( 'Auto-enabling PVCs in network configuration because --local-build-path is set in setup. ' + 'Node PVCs are required to persist custom JARs across pod restarts.', ); config.networkConfiguration[flags.getFormattedFlagKey(flags.persistentVolumeClaims)] = 'true'; } // Initialize deployment toggles with defaults if not specified config.deployMirrorNode = config.deployMirrorNode === undefined ? true : config.deployMirrorNode; config.deployExplorer = config.deployExplorer === undefined ? true : config.deployExplorer; config.deployRelay = config.deployRelay === undefined ? true : config.deployRelay; context_.createdAccounts = []; this.logger.debug(`quiet: ${config.quiet}`); return; }, }, { title: 'Acquire deployment lock', task: async ( _: OneShotSingleDeployContext, task: SoloListrTaskWrapper<OneShotSingleDeployContext>, ): Promise<Listr<OneShotSingleDeployContext>> => { oneShotLease = await this.leaseManager.create(); return ListrLock.newAcquireLockTask(oneShotLease, task); }, }, { title: 'Check for other deployments', task: async ( _: OneShotSingleDeployContext, task: SoloListrTaskWrapper<OneShotSingleDeployContext>, ): Promise<void> => { const existingRemoteConfigs: ConfigMap[] = await this.k8Factory .default() .configMaps() .listForAllNamespaces(Templates.renderConfigMapRemoteConfigLabels()); if (existingRemoteConfigs.length > 0) { const existingDeploymentsTable: string[] = remoteConfigsToDeploymentsTable(existingRemoteConfigs); const promptOptions: {default: boolean; message: string} = { default: false, message: '⚠️ Warning: Existing solo deployment detected in cluster.\n\n' + existingDeploymentsTable.join('\n') + '\n\nCreating another deployment will require additional' + ' CPU and memory resources. Do you want to proceed and create another deployment?', }; const proceed: boolean = await task .prompt(ListrInquirerPromptAdapter) .run(confirmPrompt, promptOptions); if (!proceed) { throw new SoloError('Aborted by user'); } } }, skip: (context_: OneShotSingleDeployContext): boolean => context_.config.force === true || context_.config.quiet === true, }, invokeSoloCommand( `solo ${CacheCommandDefinition.IMAGE_PULL_COMMAND}`, CacheCommandDefinition.IMAGE_PULL_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...CacheCommandDefinition.IMAGE_PULL_COMMAND.split(' '), optionFromFlag(Flags.edgeEnabled), (!!config.edgeEnabled).toString(), ); return argvPushGlobalFlags(argv); }, this.taskList, (): boolean => !constants.CONFIG.ENABLE_IMAGE_CACHE, ), invokeSoloCommand( `solo ${CacheCommandDefinition.IMAGE_LOAD_COMMAND}`, CacheCommandDefinition.IMAGE_LOAD_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...CacheCommandDefinition.IMAGE_LOAD_COMMAND.split(' '), optionFromFlag(Flags.clusterRef), config.clusterRef, ); return argvPushGlobalFlags(argv); }, this.taskList, (): boolean => !constants.CONFIG.ENABLE_IMAGE_CACHE, ), invokeSoloCommand( `solo ${ClusterReferenceCommandDefinition.CONNECT_COMMAND}`, ClusterReferenceCommandDefinition.CONNECT_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...ClusterReferenceCommandDefinition.CONNECT_COMMAND.split(' '), optionFromFlag(Flags.clusterRef), config.clusterRef, optionFromFlag(Flags.context), config.context, ); return argvPushGlobalFlags(argv); }, this.taskList, ), invokeSoloCommand( `solo ${DeploymentCommandDefinition.CREATE_COMMAND}`, DeploymentCommandDefinition.CREATE_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...DeploymentCommandDefinition.CREATE_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, optionFromFlag(Flags.namespace), config.namespace.name, ); return argvPushGlobalFlags(argv); }, this.taskList, ), invokeSoloCommand( `solo ${DeploymentCommandDefinition.ATTACH_COMMAND}`, DeploymentCommandDefinition.ATTACH_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...DeploymentCommandDefinition.ATTACH_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, optionFromFlag(Flags.clusterRef), config.clusterRef, optionFromFlag(Flags.numberOfConsensusNodes), config.numberOfConsensusNodes.toString(), ); return argvPushGlobalFlags(argv); }, this.taskList, ), invokeSoloCommand( `solo ${ClusterReferenceCommandDefinition.SETUP_COMMAND}`, ClusterReferenceCommandDefinition.SETUP_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...ClusterReferenceCommandDefinition.SETUP_COMMAND.split(' '), optionFromFlag(Flags.clusterRef), config.clusterRef, ); if (config.deployMetricsServer) { argv.push(optionFromFlag(Flags.deployMetricsServer)); } return argvPushGlobalFlags(argv); }, this.taskList, ), invokeSoloCommand( `solo ${KeysCommandDefinition.KEYS_COMMAND}`, KeysCommandDefinition.KEYS_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...KeysCommandDefinition.KEYS_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, optionFromFlag(Flags.generateGossipKeys), 'true', optionFromFlag(Flags.generateTlsKeys), ); return argvPushGlobalFlags(argv, config.cacheDir); }, this.taskList, ), { title: 'Create remote config components', task: async (): Promise<void> => { // Pre add remote config components to remote config if (constants.ONE_SHOT_WITH_BLOCK_NODE === 'true') { // Add Block Node const blockNode: BlockNodeStateSchema = this.componentFactory.createNewBlockNodeComponent( config.clusterRef, config.namespace, ); blockNode.metadata.phase = DeploymentPhase.REQUESTED; this.remoteConfig.configuration.components.addNewComponent( blockNode, ComponentTypes.BlockNode, false, true, ); } // Add Explorer if (config.deployExplorer) { const explorer: ExplorerStateSchema = this.componentFactory.createNewExplorerComponent( config.clusterRef, config.namespace, ); explorer.metadata.phase = DeploymentPhase.REQUESTED; this.remoteConfig.configuration.components.addNewComponent( explorer, ComponentTypes.Explorer, false, true, ); } // Add Mirror Node if (config.deployMirrorNode) { const mirrorNode: MirrorNodeStateSchema = this.componentFactory.createNewMirrorNodeComponent( config.clusterRef, config.namespace, ); mirrorNode.metadata.phase = DeploymentPhase.REQUESTED; this.remoteConfig.configuration.components.addNewComponent( mirrorNode, ComponentTypes.MirrorNode, false, true, ); } // Add Relay if (config.deployRelay) { const nodeIds: NodeId[] = []; for (const alias of Templates.renderNodeAliasesFromCount(config.numberOfConsensusNodes, 0)) { nodeIds.push(Templates.nodeIdFromNodeAlias(alias)); } const relay: RelayNodeStateSchema = this.componentFactory.createNewRelayComponent( config.clusterRef, config.namespace, nodeIds, ); relay.metadata.phase = DeploymentPhase.REQUESTED; this.remoteConfig.configuration.components.addNewComponent( relay, ComponentTypes.RelayNodes, false, true, ); } await this.remoteConfig.persist(); }, }, { title: 'Deploy Solo components', task: (_, task): SoloListr<OneShotSingleDeployContext> => { // Network node pipeline: deploy network node, then setup, start consensus node, and account generation // Must be sequential const deployNetworkNodeTask: SoloListrTask<OneShotSingleDeployContext> = { title: 'Deploy network node', task: async (_, networkNodeTask): Promise<SoloListr<OneShotSingleDeployContext>> => { return networkNodeTask.newListr( [ invokeSoloCommand( `solo ${ConsensusCommandDefinition.DEPLOY_COMMAND}`, ConsensusCommandDefinition.DEPLOY_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...ConsensusCommandDefinition.DEPLOY_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, ); if (config.networkConfiguration) { this.appendConfigToArgv(argv, config.networkConfiguration); } return argvPushGlobalFlags(argv, config.cacheDir); }, this.taskList, ), { title: 'Setup and Start consensus node', task: async ( _: OneShotSingleDeployContext, task: SoloListrTaskWrapper<OneShotSingleDeployContext>, ): Promise<SoloListr<OneShotSingleDeployContext>> => { return task.newListr( [ invokeSoloCommand( `solo ${ConsensusCommandDefinition.SETUP_COMMAND}`, ConsensusCommandDefinition.SETUP_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...ConsensusCommandDefinition.SETUP_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, ); this.appendConfigToArgv(argv, config.setupConfiguration); return argvPushGlobalFlags(argv, config.cacheDir); }, this.taskList, ), invokeSoloCommand( `solo ${ConsensusCommandDefinition.START_COMMAND}`, ConsensusCommandDefinition.START_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...ConsensusCommandDefinition.START_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, ); this.appendConfigToArgv(argv, { [optionFromFlag(Flags.externalAddress)]: config.externalAddress, ...config.consensusNodeConfiguration, }); return argvPushGlobalFlags(argv); }, this.taskList, ), { title: 'Create Accounts', skip: (): boolean => config.predefinedAccounts === false, task: async ( _: OneShotSingleDeployContext, task: SoloListrTaskWrapper<OneShotSingleDeployContext>, ): Promise<Listr<OneShotSingleDeployContext>> => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); const subTasks: SoloListrTask<OneShotSingleDeployContext>[] = []; const client: Client = await this.accountManager.loadNodeClient( config.namespace, this.remoteConfig.getClusterRefs(), config.deployment, ); const realm: Realm = this.localConfig.configuration.realmForDeployment( config.deployment, ); const shard: Shard = this.localConfig.configuration.shardForDeployment( config.deployment, ); // Check if Topic with ID 1001 exists, if not create a buffer topic to bump the entity ID counter // so that created accounts have IDs start from x.x.1002 try { const entity1001Query: TopicInfoQuery = new TopicInfoQuery().setTopicId( TopicId.fromString(entityId(realm, shard, 1001)), ); await entity1001Query.execute(client); } catch (error) { try { if (error.message.includes('INVALID_TOPIC_ID')) { const bufferTopic: TopicCreateTransaction = new TopicCreateTransaction().setTopicMemo('Buffer topic to bump entity IDs'); await bufferTopic.execute(client); } } catch (error) { this.logger.warn( 'Failed to create topic. Created account IDs may be offset from the expected values.', error, ); } } const accountsToCreate: PredefinedAccount[] = [...predefinedEcdsaAccountsWithAlias]; for (const [index, account] of accountsToCreate.entries()) { // inject index to avoid closure issues ((index: number, account: PredefinedAccount): void => { subTasks.push({ title: `Creating Account ${index}`, task: async ( context_: OneShotSingleDeployContext, subTask: SoloListrTaskWrapper<OneShotSingleDeployContext>, ): Promise<void> => { await helpers.sleep(Duration.ofMillis(100 * index)); const createdAccount: { accountId: string; privateKey: string; publicKey: string; balance: number; accountAlias?: string; } = await this.accountManager.createNewAccount( context_.config.namespace, account.privateKey, account.balance.to(HbarUnit.Hbar).toNumber(), account.alias, context_.config.context, ); context_.createdAccounts.push({ accountId: AccountId.fromString(createdAccount.accountId), data: account, alias: createdAccount.accountAlias, publicKey: createdAccount.publicKey, }); subTask.title = `Account created: ${createdAccount.accountId.toString()}`; }, }); })(index, account); } return task.newListr(subTasks, { concurrent: config.parallelDeploy, rendererOptions: {collapseSubtasks: false}, }); }, }, ], {concurrent: false, rendererOptions: {collapseSubtasks: false}}, ); }, }, ], {concurrent: false, rendererOptions: {collapseSubtasks: false}}, ); }, }; return task.newListr( [ invokeSoloCommand( `solo ${BlockCommandDefinition.ADD_COMMAND}`, BlockCommandDefinition.ADD_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...BlockCommandDefinition.ADD_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, ); // Build a local copy with the dev image values file appended, without mutating // config.blockNodeConfiguration — it may be an alias for another section's object // (e.g. via YAML anchors), causing the values file to leak into other commands. const blockExistingValuesFile: string = config.blockNodeConfiguration?.[flags.getFormattedFlagKey(Flags.valuesFile)]; const blockLocalConfig: AnyObject = { ...config.blockNodeConfiguration, [flags.getFormattedFlagKey(Flags.valuesFile)]: blockExistingValuesFile ? `${blockExistingValuesFile},${constants.BLOCK_NODE_SOLO_DEV_FILE}` : constants.BLOCK_NODE_SOLO_DEV_FILE, }; this.appendConfigToArgv(argv, blockLocalConfig); return argvPushGlobalFlags(argv); }, this.taskList, (): boolean => constants.ONE_SHOT_WITH_BLOCK_NODE.toLowerCase() !== 'true', ), deployNetworkNodeTask, invokeSoloCommand( `solo ${MirrorCommandDefinition.ADD_COMMAND}`, MirrorCommandDefinition.ADD_COMMAND, (): string[] => { const argv: string[] = newArgv(); argv.push( ...MirrorCommandDefinition.ADD_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, optionFromFlag(Flags.clusterRef), config.clusterRef, optionFromFlag(Flags.pinger), optionFromFlag(Flags.enableIngress), optionFromFlag(Flags.parallelDeploy), config.parallelDeploy.toString(), ); // Append HikariCP limits file without mutating the shared config object. const mirrorExistingValuesFile: string = config.mirrorNodeConfiguration?.[flags.getFormattedFlagKey(Flags.valuesFile)]; const mirrorLocalConfig: AnyObject = { [optionFromFlag(Flags.externalAddress)]: config.externalAddress, ...config.mirrorNodeConfiguration, [flags.getFormattedFlagKey(Flags.valuesFile)]: mirrorExistingValuesFile ? `${mirrorExistingValuesFile},${constants.MIRROR_NODE_HIKARI_LIMITS_FILE}` : constants.MIRROR_NODE_HIKARI_LIMITS_FILE, }; this.appendConfigToArgv(argv, mirrorLocalConfig); return argvPushGlobalFlags(argv, config.cacheDir); }, this.taskList, (): boolean => !config.deployMirrorNode, ), invokeSoloCommand( `solo ${ExplorerCommandDefinition.ADD_COMMAND}`, ExplorerCommandDefinition.ADD_COMMAND, async (): Promise<string[]> => { await this.eventBus.waitFor( SoloEventType.MirrorNodeDeployed, (soloEvent: MirrorNodeDeployedEvent): boolean => soloEvent.deployment === config.deployment, Duration.ofMinutes(5), ); const argv: string[] = newArgv(); argv.push( ...ExplorerCommandDefinition.ADD_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, optionFromFlag(Flags.clusterRef), config.clusterRef, ); this.appendConfigToArgv(argv, { [optionFromFlag(Flags.externalAddress)]: config.externalAddress, [optionFromFlag(Flags.explorerVersion)]: config.versions.explorer, [optionFromFlag(Flags.mirrorNodeId)]: mirrorNodeId, [optionFromFlag(Flags.mirrorNamespace)]: config.namespace.name, ...config.explorerNodeConfiguration, }); return argvPushGlobalFlags(argv, config.cacheDir); }, this.taskList, (): boolean => !config.deployExplorer && !config.minimalSetup, ), invokeSoloCommand( `solo ${RelayCommandDefinition.ADD_COMMAND}`, RelayCommandDefinition.ADD_COMMAND, async (): Promise<string[]> => { await this.eventBus.waitFor( SoloEventType.MirrorNodeDeployed, (soloEvent: MirrorNodeDeployedEvent): boolean => soloEvent.deployment === config.deployment, Duration.ofMinutes(5), ); await this.eventBus.waitFor( SoloEventType.NodesStarted, (soloEvent: NodesStartedEvent): boolean => soloEvent.deployment === config.deployment, Duration.ofMinutes(5), ); const argv: string[] = newArgv(); argv.push( ...RelayCommandDefinition.ADD_COMMAND.split(' '), optionFromFlag(Flags.deployment), config.deployment, optionFromFlag(Flags.clusterRef), config.clusterRef, optionFromFlag(Flags.nodeAliasesUnparsed), 'node1', ); this.appendConfigToArgv(argv, { [optionFromFlag(Flags.externalAddress)]: config.externalAddress, [optionFromFlag(Flags.mirrorNodeId)]: mirrorNodeId, [optionFromFlag(Flags.mirrorNamespace)]: config.namespace.name, ...config.relayNodeConfiguration, }); return argvPushGlobalFlags(argv); }, this.taskList, (): boolean => !config.deployRelay && !config.minimalSetup, ), ], {concurrent: config.parallelDeploy, rendererOptions: {collapseSubtasks: false}}, ); }, }, { title: 'Finish', task: async (context_: OneShotSingleDeployContext): Promise<void> => { const outputDirectory: string = this.getOneShotOutputDirectory(context_.config.deployment); this.logger.info(`Output directory: ${outputDirectory}`); this.showOneShotUserNotes(context_, false, PathEx.join(outputDirectory, 'notes')); this.showVersions(PathEx.join(outputDirectory, 'versions'), config); this.showPortForwards(PathEx.join(outputDirectory, 'forwards')); this.showAccounts(context_.createdAccounts, context_, PathEx.join(outputDirectory, 'accounts.json')); this.cacheDeploymentName(context_, PathEx.join(constants.SOLO_CACHE_DIR, 'last-one-shot-deployment.txt')); return; }, }, ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, ); try {