UNPKG

@hashgraph/solo

Version:

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

735 lines 81.6 kB
// SPDX-License-Identifier: Apache-2.0 var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var DefaultOneShotCommand_1; import { Listr } 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 { inject, injectable } from 'tsyringe-neo'; import { NamespaceName } from '../../types/namespace/namespace-name.js'; import { StringEx } from '../../business/utils/string-ex.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 { PREDEFINED_ACCOUNT_GROUPS, predefinedEcdsaAccountsWithAlias, } from './predefined-accounts.js'; import { AccountId, 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 { Templates } from '../../core/templates.js'; import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer'; import { SemanticVersion } from '../../business/utils/semantic-version.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 { DeploymentPhase } from '../../data/schema/model/remote/deployment-phase.js'; import { ComponentTypes } from '../../core/config/remote/enumerations/component-types.js'; import { SoloEventType } from '../../core/events/event-types/solo-event.js'; import { CacheCommandDefinition } from '../command-definitions/cache-command-definition.js'; let DefaultOneShotCommand = class DefaultOneShotCommand extends BaseCommand { static { DefaultOneShotCommand_1 = this; } accountManager; eventBus; static SINGLE_DEPLOY_CONFIGS_NAME = 'singleAddConfigs'; static SINGLE_DESTROY_CONFIGS_NAME = 'singleDestroyConfigs'; _isRollback = false; static DEPLOY_FLAGS_LIST = { required: [], optional: [ flags.quiet, flags.force, flags.deployment, flags.namespace, flags.clusterRef, flags.minimalSetup, flags.rollback, flags.parallelDeploy, flags.externalAddress, flags.edgeEnabled, ], }; static MULTI_DEPLOY_FLAGS_LIST = { required: [], optional: [...DefaultOneShotCommand_1.DEPLOY_FLAGS_LIST.optional, flags.numberOfConsensusNodes], }; static DESTROY_FLAGS_LIST = { required: [], optional: [flags.quiet, flags.deployment], }; static FALCON_DEPLOY_FLAGS_LIST = { 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, ], }; static FALCON_DESTROY_FLAGS_LIST = { required: [], optional: [...DefaultOneShotCommand_1.DESTROY_FLAGS_LIST.optional], }; static INFO_FLAGS_LIST = { required: [], optional: [flags.quiet, flags.deployment], }; constructor(accountManager, eventBus) { super(); this.accountManager = accountManager; this.eventBus = eventBus; 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. */ concatConfigFiles(defaultFilePath, overrideFilePath, outputFilePath) { const defaultContent = fs.existsSync(defaultFilePath) ? fs.readFileSync(defaultFilePath, 'utf8') : ''; const overrideContent = fs.existsSync(overrideFilePath) ? fs.readFileSync(overrideFilePath, 'utf8') : ''; const outputDirectory = 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 */ appendConfigToArgv(argv, configSection) { 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()); } } } async deploy(argv) { return this.deployInternal(argv, DefaultOneShotCommand_1.DEPLOY_FLAGS_LIST); } async deployFalcon(argv) { return this.deployInternal(argv, DefaultOneShotCommand_1.FALCON_DEPLOY_FLAGS_LIST); } async performRollback(deployError, config) { 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 = { _: [], deployment: config.deployment, clusterRef: config.clusterRef, namespace: config.namespace.name, context: config.context, quiet: true, }; this._isRollback = true; try { await this.destroyInternal(destroyArgv, DefaultOneShotCommand_1.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 = 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); } async deployInternal(argv, flagsList) { let config = undefined; let oneShotLease; const mirrorNodeId = 1; const tasks = this.taskList.newOneShotSingleDeployTaskList([ { title: 'Initialize', task: async (context_, task) => { this.configManager.update(argv); this.oneShotState.activate(); const edgeEnabled = this.configManager.getFlag(Flags.edgeEnabled); const versions = 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 = [...flagsList.required, ...flagsList.optional]; await this.configManager.executePrompt(task, allFlags); context_.config = this.configManager.getConfig(DefaultOneShotCommand_1.SINGLE_DEPLOY_CONFIGS_NAME, allFlags); 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 = fs.readFileSync(context_.config.valuesFile, 'utf8'); const profileItems = yaml.parse(valuesFileContent); // 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 = 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 = 'v0.72.0-0'; const MINIMUM_CN_VERSION_FOR_STATE_ON_DISK = 'v0.73.0-0'; const cnVersion = new SemanticVersion(versions.consensus); if (!config.valuesFile && cnVersion.greaterThanOrEqual(MINIMUM_CN_VERSION_FOR_SMALL_MEMORY)) { const defaultsDirectory = PathEx.join(constants.SOLO_CACHE_DIR, 'templates'); const overridesDirectory = PathEx.join(defaultsDirectory, 'small-memory'); const stateOnDiskDirectory = PathEx.join(defaultsDirectory, 'small-memory-state-on-disk'); const mergedDirectory = PathEx.join(defaultsDirectory, 'small-memory-merged'); const settingsOverrideFile = config.numberOfConsensusNodes > 1 ? 'settings-multinode.txt' : 'settings-single.txt'; const useStateOnDisk = cnVersion.greaterThanOrEqual(MINIMUM_CN_VERSION_FOR_STATE_ON_DISK); const settingsMergedPath = 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 = 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 = 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 (_, task) => { oneShotLease = await this.leaseManager.create(); return ListrLock.newAcquireLockTask(oneShotLease, task); }, }, { title: 'Check for other deployments', task: async (_, task) => { const existingRemoteConfigs = await this.k8Factory .default() .configMaps() .listForAllNamespaces(Templates.renderConfigMapRemoteConfigLabels()); if (existingRemoteConfigs.length > 0) { const existingDeploymentsTable = remoteConfigsToDeploymentsTable(existingRemoteConfigs); const promptOptions = { 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 = await task .prompt(ListrInquirerPromptAdapter) .run(confirmPrompt, promptOptions); if (!proceed) { throw new SoloError('Aborted by user'); } } }, skip: (context_) => context_.config.force === true || context_.config.quiet === true, }, invokeSoloCommand(`solo ${CacheCommandDefinition.IMAGE_PULL_COMMAND}`, CacheCommandDefinition.IMAGE_PULL_COMMAND, () => { const argv = newArgv(); argv.push(...CacheCommandDefinition.IMAGE_PULL_COMMAND.split(' '), optionFromFlag(Flags.edgeEnabled), (!!config.edgeEnabled).toString()); return argvPushGlobalFlags(argv); }, this.taskList, () => !constants.CONFIG.ENABLE_IMAGE_CACHE), invokeSoloCommand(`solo ${CacheCommandDefinition.IMAGE_LOAD_COMMAND}`, CacheCommandDefinition.IMAGE_LOAD_COMMAND, () => { const argv = newArgv(); argv.push(...CacheCommandDefinition.IMAGE_LOAD_COMMAND.split(' '), optionFromFlag(Flags.clusterRef), config.clusterRef); return argvPushGlobalFlags(argv); }, this.taskList, () => !constants.CONFIG.ENABLE_IMAGE_CACHE), invokeSoloCommand(`solo ${ClusterReferenceCommandDefinition.CONNECT_COMMAND}`, ClusterReferenceCommandDefinition.CONNECT_COMMAND, () => { const argv = 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, () => { const argv = 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, () => { const argv = 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, () => { const argv = 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, () => { const argv = 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 () => { // Pre add remote config components to remote config if (constants.ONE_SHOT_WITH_BLOCK_NODE === 'true') { // Add Block Node const blockNode = 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 = 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 = 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 = []; for (const alias of Templates.renderNodeAliasesFromCount(config.numberOfConsensusNodes, 0)) { nodeIds.push(Templates.nodeIdFromNodeAlias(alias)); } const relay = 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) => { // Network node pipeline: deploy network node, then setup, start consensus node, and account generation // Must be sequential const deployNetworkNodeTask = { title: 'Deploy network node', task: async (_, networkNodeTask) => { return networkNodeTask.newListr([ invokeSoloCommand(`solo ${ConsensusCommandDefinition.DEPLOY_COMMAND}`, ConsensusCommandDefinition.DEPLOY_COMMAND, () => { const argv = 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 (_, task) => { return task.newListr([ invokeSoloCommand(`solo ${ConsensusCommandDefinition.SETUP_COMMAND}`, ConsensusCommandDefinition.SETUP_COMMAND, () => { const argv = 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, () => { const argv = 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: () => config.predefinedAccounts === false, task: async (_, task) => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); const subTasks = []; const client = await this.accountManager.loadNodeClient(config.namespace, this.remoteConfig.getClusterRefs(), config.deployment); const realm = this.localConfig.configuration.realmForDeployment(config.deployment); const 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 = 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 = 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 = [...predefinedEcdsaAccountsWithAlias]; for (const [index, account] of accountsToCreate.entries()) { // inject index to avoid closure issues ((index, account) => { subTasks.push({ title: `Creating Account ${index}`, task: async (context_, subTask) => { await helpers.sleep(Duration.ofMillis(100 * index)); const createdAccount = 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, () => { const argv = 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 = config.blockNodeConfiguration?.[flags.getFormattedFlagKey(Flags.valuesFile)]; const blockLocalConfig = { ...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, () => constants.ONE_SHOT_WITH_BLOCK_NODE.toLowerCase() !== 'true'), deployNetworkNodeTask, invokeSoloCommand(`solo ${MirrorCommandDefinition.ADD_COMMAND}`, MirrorCommandDefinition.ADD_COMMAND, () => { const argv = 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 = config.mirrorNodeConfiguration?.[flags.getFormattedFlagKey(Flags.valuesFile)]; const mirrorLocalConfig = { [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, () => !config.deployMirrorNode), invokeSoloCommand(`solo ${ExplorerCommandDefinition.ADD_COMMAND}`, ExplorerCommandDefinition.ADD_COMMAND, async () => { await this.eventBus.waitFor(SoloEventType.MirrorNodeDeployed, (soloEvent) => soloEvent.deployment === config.deployment, Duration.ofMinutes(5)); const argv = 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, () => !config.deployExplorer && !config.minimalSetup), invokeSoloCommand(`solo ${RelayCommandDefinition.ADD_COMMAND}`, RelayCommandDefinition.ADD_COMMAND, async () => { await this.eventBus.waitFor(SoloEventType.MirrorNodeDeployed, (soloEvent) => soloEvent.deployment === config.deployment, Duration.ofMinutes(5)); await this.eventBus.waitFor(SoloEventType.NodesStarted, (soloEvent) => soloEvent.deployment === config.deployment, Duration.ofMinutes(5)); const argv = 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, () => !config.deployRelay && !config.minimalSetup), ], { concurrent: config.parallelDeploy, rendererOptions: { collapseSubtasks: false } }); }, }, { title: 'Finish', task: async (context_) => { const outputDirectory = 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 { await tasks.run(); } catch (error) { await this.performRollback(error, config); } finally { this.oneShotState.deactivate(); const cleanupPromises = []; if (oneShotLease) { cleanupPromises.push(oneShotLease.release(true).catch((error) => { this.logger.error('Error releasing one-shot lease:', error); })); } cleanupPromises.push(this.taskList .callCloseFunctions() .then() .catch((error) => { this.logger.error('Error during closing task list:', error); })); await Promise.all(cleanupPromises); } return true; } showOneShotUserNotes(context_, isMultiple = false, outputFile) { const messageGroupKey = isMultiple ? 'one-shot-multiple-user-notes' : 'one-shot-user-notes'; const title = isMultiple ? 'One Shot Multiple User Notes' : 'One Shot User Notes'; this.logger.addMessageGroup(messageGroupKey, title); const data = [ `Cluster Reference: ${context_.config.clusterRef}`, `Deployment Name: ${context_.config.deployment}`, `Namespace Name: ${context_.config.namespace.name}`, ]; for (const line of data) { this.logger.addMessageGroupMessage(messageGroupKey, line); } if (isMultiple) { this.logger.addMessageGroupMessage(messageGroupKey, `Number of Consensus Nodes: ${context_.config.numberOfConsensusNodes}`); } this.logger.addMessageGroupMessage(messageGroupKey, 'To quickly delete the deployed resources, run the following command:\n' + `kubectl delete ns ${context_.config.namespace.name}`); this.logger.showMessageGroup(messageGroupKey); if (outputFile) { const fileData = data.join('\n') + '\n'; createDirectoryIfNotExists(outputFile); fs.writeFileSync(outputFile, fileData); this.logger.showUser(chalk.green(`✅ User notes saved to file: ${outputFile}`)); } } showVersions(outputFile, config) { const messageGroupKey = 'versions-used'; this.logger.addMessageGroup(messageGroupKey, 'Versions Used'); const data = [ `Solo Chart Version: ${config.versions.soloChart}`, `Consensus Node Version: ${config.versions.consensus}`, `Mirror Node Version: ${config.versions.mirror}`, `Explorer Version: ${config.versions.explorer}`, `JSON RPC Relay Version: ${config.versions.relay}`, ]; for (const line of data) { this.logger.addMessageGroupMessage(messageGroupKey, line); } this.logger.showMessageGroup(messageGroupKey); if (outputFile) { const fileData = data.join('\n') + '\n'; createDirectoryIfNotExists(outputFile); fs.writeFileSync(outputFile, fileData); this.logger.showUser(chalk.green(`✅ Versions used saved to file: ${outputFile}`)); } } cacheDeploymentName(context, outputFile) { fs.writeFileSync(outputFile, context.config.deployment); this.logger.showUser(chalk.green(`✅ Deployment name (${context.config.deployment}) saved to file: ${outputFile}`)); } getOneShotOutputDirectory(deploymentName) { return PathEx.join(constants.SOLO_HOME_DIR, `one-shot-${deploymentName}`); } showAccounts(createdAccounts = [], context, outputFile) { if (createdAccounts.length > 0) { createdAccounts.sort((a, b) => a.accountId.compare(b.accountId)); const ecdsaAccounts = createdAccounts.filter((account) => account.data.group === PREDEFINED_ACCOUNT_GROUPS.ECDSA); const aliasAccounts = createdAccounts.filter((account) => account.data.group === PREDEFINED_ACCOUNT_GROUPS.ECDSA_ALIAS); const ed25519Accounts = createdAccounts.filter((account) => account.data.group === PREDEFINED_ACCOUNT_GROUPS.ED25519); const systemAccountsGroupKey = 'system-accounts'; const messageGroupKey = 'accounts-created'; const ecdsaGroupKey = 'accounts-created-ecdsa'; const ecdsaAliasGroupKey = 'accounts-created-ecdsa-alias'; const ed25519GroupKey = 'accounts-created-ed25519'; const realm = this.localConfig.configuration.realmForDeployment(context.config.deployment); const shard = this.localConfig.configuration.shardForDeployment(context.config.deployment); const operatorAccountData = { name: 'Operator', accountId: entityId(shard, realm, 2), publicKey: constants.GENESIS_PUBLIC_KEY, }; if (constants.GENESIS_KEY === constants.DEFAULT_GENESIS_KEY) { operatorAccountData.privateKey = constants.DEFAULT_GENESIS_KEY; } const systemAccounts = [operatorAccountData]; if (systemAccounts.length > 0) { this.logger.addMessageGroup(systemAccountsGroupKey, 'System Accounts'); for (const account of systemAccounts) { let message = `${account.name} Account ID: ${account.accountId.toString()}, Public Key: ${account.publicKey.toString()}`; if (account.privateKey) { message += `, Private Key: ${account.privateKey}`; } this.logger.addMessageGroupMessage(systemAccountsGroupKey, message); } this.logger.showMessageGroup(systemAccountsGroupKey); } this.logger.addMessageGroup(messageGroupKey, 'Created Accounts'); this.logger.addMessageGroup(ecdsaGroupKey, 'ECDSA Accounts (Not EVM compatible, See ECDSA Alias Accounts above)'); this.logger.addMessageGroup(ecdsaAliasGroupKey, 'ECDSA Alias Accounts (EVM compatible)'); this.logger.addMessageGroup(ed25519GroupKey, 'ED25519 Accounts'); if (aliasAccounts.length > 0) { for (const account of aliasAccounts) { this.logger.addMessageGroupMessage(ecdsaAliasGroupKey, `Account ID: ${account.accountId.toString()}, Public address: $