@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
735 lines • 81.6 kB
JavaScript
// 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: $