diff --git a/src/commands/account.ts b/src/commands/account.ts index a0ab148d3..9513b6a41 100644 --- a/src/commands/account.ts +++ b/src/commands/account.ts @@ -14,18 +14,16 @@ * limitations under the License. * */ - import chalk from 'chalk' import { BaseCommand } from './base.ts' import { SoloError, IllegalArgumentError } from '../core/errors.ts' import { flags } from './index.ts' import { Listr } from 'listr2' import * as prompts from './prompts.ts' -import { constants } from '../core/index.ts' +import { constants, type AccountManager } from '../core/index.ts' import { type AccountId, AccountInfo, HbarUnit, PrivateKey } from '@hashgraph/sdk' import { FREEZE_ADMIN_ACCOUNT } from '../core/constants.ts' -import { type AccountManager } from '../core/account_manager.ts' -import { type Opts } from '../types/index.ts' +import type { Opts } from '../types/index.ts' export class AccountCommand extends BaseCommand { private readonly accountManager: AccountManager @@ -249,6 +247,7 @@ export class AccountCommand extends BaseCommand { async create (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface Context { config: { @@ -292,6 +291,8 @@ export class AccountCommand extends BaseCommand { self.logger.debug('Initialized config', { config }) await self.accountManager.loadNodeClient(ctx.config.namespace) + + return lease.buildAcquireTask(task) } }, { @@ -314,6 +315,7 @@ export class AccountCommand extends BaseCommand { } catch (e: Error | any) { throw new SoloError(`Error in creating account: ${e.message}`, e) } finally { + await lease.release() await this.closeConnections() } diff --git a/src/commands/base.ts b/src/commands/base.ts index 7e3ad38a3..011f8a90b 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -18,9 +18,8 @@ import paths from 'path' import { MissingArgumentError } from '../core/errors.ts' import { ShellRunner } from '../core/shell_runner.ts' -import { type ChartManager, type ConfigManager, type Helm, type K8 } from '../core/index.ts' -import { type DependencyManager } from '../core/dependency_managers/index.ts' -import { type CommandFlag, type Opts } from '../types/index.ts' +import type { ChartManager, ConfigManager, Helm, K8, DependencyManager, LeaseManager } from '../core/index.ts' +import type { CommandFlag, Opts } from '../types/index.ts' export class BaseCommand extends ShellRunner { protected readonly helm: Helm @@ -28,7 +27,8 @@ export class BaseCommand extends ShellRunner { protected readonly chartManager: ChartManager protected readonly configManager: ConfigManager protected readonly depManager: DependencyManager - protected readonly _configMaps: Map + protected readonly leaseManager: LeaseManager + protected readonly _configMaps = new Map() constructor (opts: Opts) { if (!opts || !opts.logger) throw new Error('An instance of core/SoloLogger is required') @@ -45,7 +45,7 @@ export class BaseCommand extends ShellRunner { this.chartManager = opts.chartManager this.configManager = opts.configManager this.depManager = opts.depManager - this._configMaps = new Map() + this.leaseManager = opts.leaseManager } async prepareChartPath (chartDir: string, chartRepo: string, chartReleaseName: string) { diff --git a/src/commands/cluster.ts b/src/commands/cluster.ts index 0992ee2d1..769935594 100644 --- a/src/commands/cluster.ts +++ b/src/commands/cluster.ts @@ -85,7 +85,6 @@ export class ClusterCommand extends BaseCommand { flags.deployPrometheusStack ]) - // prepare config ctx.config = { chartDir: self.configManager.getFlag(flags.chartDirectory) as string, clusterSetupNamespace: self.configManager.getFlag(flags.clusterSetupNamespace) as string, @@ -160,6 +159,7 @@ export class ClusterCommand extends BaseCommand { async reset (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface Context { config: { @@ -195,6 +195,8 @@ export class ClusterCommand extends BaseCommand { if (!ctx.isChartInstalled) { throw new SoloError('No chart found for the cluster') } + + return lease.buildAcquireTask(task) } }, { @@ -217,6 +219,8 @@ export class ClusterCommand extends BaseCommand { await tasks.run() } catch (e: Error | any) { throw new SoloError('Error on cluster reset', e) + } finally { + await lease.release() } return true diff --git a/src/commands/flags.ts b/src/commands/flags.ts index c8eed7b76..8e11df825 100644 --- a/src/commands/flags.ts +++ b/src/commands/flags.ts @@ -18,7 +18,7 @@ import { constants } from '../core/index.ts' import * as core from '../core/index.ts' import * as version from '../../version.ts' import path from 'path' -import { type CommandFlag } from '../types/index.ts' +import type { CommandFlag } from '../types/index.ts' /** * Set flag from the flag option diff --git a/src/commands/mirror_node.ts b/src/commands/mirror_node.ts index c96b5d3a3..8055741c9 100644 --- a/src/commands/mirror_node.ts +++ b/src/commands/mirror_node.ts @@ -17,12 +17,11 @@ import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer' import { Listr } from 'listr2' import { SoloError, IllegalArgumentError, MissingArgumentError } from '../core/errors.ts' -import { constants, type ProfileManager } from '../core/index.ts' +import { constants, type ProfileManager, type AccountManager } from '../core/index.ts' import { BaseCommand } from './base.ts' import * as flags from './flags.ts' import * as prompts from './prompts.ts' import { getFileContents, getEnvValue } from '../core/helpers.ts' -import { type AccountManager } from '../core/account_manager.ts' import { type PodName } from '../types/aliases.ts' import { type Opts } from '../types/index.ts' @@ -61,13 +60,6 @@ export class MirrorNodeCommand extends BaseCommand { ] } - /** - * @param tlsClusterIssuerType - * @param enableHederaExplorerTls - * @param namespace - * @param hederaExplorerTlsLoadBalancerIp - * @param hederaExplorerTlsHostName - */ getTlsValueArguments (tlsClusterIssuerType: string, enableHederaExplorerTls: boolean, namespace: string, hederaExplorerTlsLoadBalancerIp: string, hederaExplorerTlsHostName: string) { let valuesArg = '' @@ -126,6 +118,7 @@ export class MirrorNodeCommand extends BaseCommand { async deploy (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface MirrorNodeDeployConfigClass { chartDirectory: string @@ -184,6 +177,8 @@ export class MirrorNodeCommand extends BaseCommand { } await self.accountManager.loadNodeClient(ctx.config.namespace) + + return lease.buildAcquireTask(task) } }, { @@ -327,6 +322,7 @@ export class MirrorNodeCommand extends BaseCommand { } catch (e: Error | any) { throw new SoloError(`Error starting node: ${e.message}`, e) } finally { + await lease.release() await self.accountManager.close() } @@ -335,6 +331,7 @@ export class MirrorNodeCommand extends BaseCommand { async destroy (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface Context { config: { @@ -384,6 +381,8 @@ export class MirrorNodeCommand extends BaseCommand { } await self.accountManager.loadNodeClient(ctx.config.namespace) + + return lease.buildAcquireTask(task) } }, { @@ -425,6 +424,7 @@ export class MirrorNodeCommand extends BaseCommand { } catch (e: Error | any) { throw new SoloError(`Error starting node: ${e.message}`, e) } finally { + await lease.release() await self.accountManager.close() } diff --git a/src/commands/network.ts b/src/commands/network.ts index fa5b5857d..e0f58fb8b 100644 --- a/src/commands/network.ts +++ b/src/commands/network.ts @@ -103,7 +103,8 @@ export class NetworkCommand extends BaseCommand { async prepareValuesArg (config: {chartDirectory?: string; app?: string; nodeAliases?: string[]; debugNodeAlias?: NodeAlias; enablePrometheusSvcMonitor?: boolean; releaseTag?: string; persistentVolumeClaims?: string; - valuesFile?: string; } = {}) { + valuesFile?: string; } = {} + ) { let valuesArg = config.chartDirectory ? `-f ${path.join(config.chartDirectory, 'solo-deployment', 'values.yaml')}` : '' if (config.app !== constants.HEDERA_APP_NAME) { @@ -216,6 +217,7 @@ export class NetworkCommand extends BaseCommand { /** Run helm install and deploy network components */ async deploy (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface Context { config: NetworkDeployConfigClass @@ -226,6 +228,7 @@ export class NetworkCommand extends BaseCommand { title: 'Initialize', task: async (ctx, task) => { ctx.config = await self.prepareConfig(task, argv) + return lease.buildAcquireTask(task) } }, { @@ -383,6 +386,8 @@ export class NetworkCommand extends BaseCommand { await tasks.run() } catch (e: Error | any) { throw new SoloError(`Error installing chart ${constants.SOLO_DEPLOYMENT_CHART}`, e) + } finally { + await lease.release() } return true @@ -390,6 +395,7 @@ export class NetworkCommand extends BaseCommand { async destroy (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface Context { config: { @@ -427,6 +433,8 @@ export class NetworkCommand extends BaseCommand { deleteSecrets: self.configManager.getFlag(flags.deleteSecrets) as boolean, namespace: self.configManager.getFlag(flags.namespace) as string } + + return lease.buildAcquireTask(task) } }, { @@ -470,6 +478,8 @@ export class NetworkCommand extends BaseCommand { await tasks.run() } catch (e: Error | any) { throw new SoloError('Error destroying network', e) + } finally { + await lease.release() } return true @@ -478,6 +488,7 @@ export class NetworkCommand extends BaseCommand { /** Run helm upgrade to refresh network components with new settings */ async refresh (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface Context { config: NetworkDeployConfigClass @@ -488,6 +499,7 @@ export class NetworkCommand extends BaseCommand { title: 'Initialize', task: async (ctx, task) => { ctx.config = await self.prepareConfig(task, argv) + return lease.buildAcquireTask(task) } }, { @@ -520,6 +532,8 @@ export class NetworkCommand extends BaseCommand { await tasks.run() } catch (e: Error | any) { throw new SoloError(`Error upgrading chart ${constants.SOLO_DEPLOYMENT_CHART}`, e) + } finally { + await lease.release() } return true diff --git a/src/commands/node/configs.ts b/src/commands/node/configs.ts index 401200cb4..e0c5a18d5 100644 --- a/src/commands/node/configs.ts +++ b/src/commands/node/configs.ts @@ -23,8 +23,8 @@ import path from 'path' import fs from 'fs' import { validatePath } from '../../core/helpers.ts' import * as flags from '../flags.ts' -import { type NodeAlias, type NodeAliases, type PodName } from '../../types/aliases.js' -import { type NetworkNodeServices } from '../../core/network_node_services.js' +import { type NodeAlias, type NodeAliases, type PodName } from '../../types/aliases.ts' +import { type NetworkNodeServices } from '../../core/network_node_services.ts' export const PREPARE_UPGRADE_CONFIGS_NAME = 'prepareUpgradeConfig' export const DOWNLOAD_GENERATED_FILES_CONFIGS_NAME = 'downloadGeneratedFilesConfig' diff --git a/src/commands/node/handlers.ts b/src/commands/node/handlers.ts index 064915141..e5b3ab760 100644 --- a/src/commands/node/handlers.ts +++ b/src/commands/node/handlers.ts @@ -18,9 +18,7 @@ import * as helpers from '../../core/helpers.ts' import * as NodeFlags from './flags.ts' import { - addConfigBuilder, - deleteConfigBuilder, - downloadGeneratedFilesConfigBuilder, keysConfigBuilder, logsConfigBuilder, + addConfigBuilder, deleteConfigBuilder, downloadGeneratedFilesConfigBuilder, keysConfigBuilder, logsConfigBuilder, prepareUpgradeConfigBuilder, refreshConfigBuilder, setupConfigBuilder, startConfigBuilder, stopConfigBuilder, updateConfigBuilder } from './configs.ts' @@ -29,12 +27,14 @@ import { constants, type K8, type PlatformInstaller, + type AccountManager, + type LeaseManager } from '../../core/index.ts' import { IllegalArgumentError } from '../../core/errors.ts' -import type { AccountManager } from '../../core/account_manager.js' -import type { SoloLogger } from '../../core/logging.js' -import { type NodeCommand } from './index.js' -import { type NodeCommandTasks } from './tasks.js' +import type { SoloLogger } from '../../core/logging.ts' +import type { NodeCommand } from './index.ts' +import type { NodeCommandTasks } from './tasks.ts' +import type { LeaseWrapper } from '../../core/lease_wrapper.ts' export class NodeCommandHandlers { private readonly accountManager: AccountManager @@ -43,13 +43,14 @@ export class NodeCommandHandlers { private readonly logger: SoloLogger private readonly k8: K8 private readonly tasks: NodeCommandTasks + private readonly leaseManager: LeaseManager private getConfig: any private prepareChartPath: any public readonly parent: NodeCommand - constructor (opts) { + constructor (opts: any) { if (!opts || !opts.accountManager) throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager) if (!opts || !opts.configManager) throw new Error('An instance of core/ConfigManager is required') if (!opts || !opts.logger) throw new Error('An instance of core/Logger is required') @@ -63,32 +64,18 @@ export class NodeCommandHandlers { this.configManager = opts.configManager this.k8 = opts.k8 this.platformInstaller = opts.platformInstaller + this.leaseManager = opts.leaseManager this.getConfig = opts.parent.getConfig.bind(opts.parent) this.prepareChartPath = opts.parent.prepareChartPath.bind(opts.parent) this.parent = opts.parent } - /** - * @returns {string} - */ - static get ADD_CONTEXT_FILE () { - return 'node-add.json' - } - - /** - * @returns {string} - */ - static get DELETE_CONTEXT_FILE () { - return 'node-delete.json' - } + static readonly ADD_CONTEXT_FILE = 'node-add.json' + static readonly DELETE_CONTEXT_FILE = 'node-delete.json' - /** - * stops and closes the port forwards - * @returns {Promise} - */ async close () { - this.accountManager.close() + await this.accountManager.close() if (this.parent._portForwards) { for (const srv of this.parent._portForwards) { await this.k8.stopPortForward(srv) @@ -100,9 +87,9 @@ export class NodeCommandHandlers { /** ******** Task Lists **********/ - deletePrepareTaskList (argv) { + deletePrepareTaskList (argv: any, lease: LeaseWrapper) { return [ - this.tasks.initialize(argv, deleteConfigBuilder.bind(this)), + this.tasks.initialize(argv, deleteConfigBuilder.bind(this), lease), this.tasks.identifyExistingNodes(), this.tasks.loadAdminKey(), this.tasks.prepareUpgradeZip(), @@ -110,7 +97,7 @@ export class NodeCommandHandlers { ] } - deleteSubmitTransactionsTaskList (argv) { + deleteSubmitTransactionsTaskList (argv: any) { return [ this.tasks.sendNodeDeleteTransaction(), this.tasks.sendPrepareUpgradeTransaction(), @@ -118,7 +105,7 @@ export class NodeCommandHandlers { ] } - deleteExecuteTaskList (argv) { + deleteExecuteTaskList (argv: any) { return [ this.tasks.downloadNodeGeneratedFiles(), this.tasks.prepareStagingDirectory('existingNodeAliases'), @@ -143,9 +130,9 @@ export class NodeCommandHandlers { ] } - addPrepareTasks (argv) { + addPrepareTasks (argv: any, lease: LeaseWrapper) { return [ - this.tasks.initialize(argv, addConfigBuilder.bind(this)), + this.tasks.initialize(argv, addConfigBuilder.bind(this), lease), this.tasks.checkPVCsEnabled(), this.tasks.identifyExistingNodes(), this.tasks.determineNewNodeAccountNumber(), @@ -160,7 +147,7 @@ export class NodeCommandHandlers { ] } - addSubmitTransactionsTasks (argv) { + addSubmitTransactionsTasks (argv: any) { return [ this.tasks.sendNodeCreateTransaction(), this.tasks.sendPrepareUpgradeTransaction(), @@ -168,7 +155,7 @@ export class NodeCommandHandlers { ] } - addExecuteTasks (argv) { + addExecuteTasks (argv: any) { return [ this.tasks.downloadNodeGeneratedFiles(), this.tasks.prepareStagingDirectory('allNodeAliases'), @@ -195,55 +182,66 @@ export class NodeCommandHandlers { /** ******** Handlers **********/ - async prepareUpgrade (argv) { + async prepareUpgrade (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DEFAULT_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, prepareUpgradeConfigBuilder.bind(this)), + this.tasks.initialize(argv, prepareUpgradeConfigBuilder.bind(this), lease), this.tasks.prepareUpgradeZip(), this.tasks.sendPrepareUpgradeTransaction() ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in preparing node upgrade') + }, 'Error in preparing node upgrade', lease) await action(argv, this) return true } - async freezeUpgrade (argv) { + async freezeUpgrade (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DEFAULT_FLAGS) + + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, prepareUpgradeConfigBuilder.bind(this)), + this.tasks.initialize(argv, prepareUpgradeConfigBuilder.bind(this), null), this.tasks.prepareUpgradeZip(), this.tasks.sendFreezeUpgradeTransaction() ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in executing node freeze upgrade') + }, 'Error in executing node freeze upgrade', null) await action(argv, this) return true } - async downloadGeneratedFiles (argv) { + async downloadGeneratedFiles (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DEFAULT_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, downloadGeneratedFilesConfigBuilder.bind(this)), + this.tasks.initialize(argv, downloadGeneratedFilesConfigBuilder.bind(this), lease), this.tasks.identifyExistingNodes(), this.tasks.downloadNodeGeneratedFiles() ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in downloading generated files') + }, 'Error in downloading generated files', lease) await action(argv, this) return true } - async update (argv) { + async update (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, updateConfigBuilder.bind(this)), + this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), this.tasks.identifyExistingNodes(), this.tasks.prepareGossipEndpoints(), this.tasks.prepareGrpcServiceEndpoints(), @@ -260,7 +258,7 @@ export class NodeCommandHandlers { this.tasks.getNodeLogsAndConfigs(), this.tasks.updateChartWithConfigMap( 'Update chart to use new configMap due to account number change', - (ctx) => !ctx.config.newAccountNumber && !ctx.config.debugNodeAlias + (ctx: any) => !ctx.config.newAccountNumber && !ctx.config.debugNodeAlias ), this.tasks.killNodesAndUpdateConfigMap(), this.tasks.checkNodePodsAreRunning(), @@ -278,149 +276,176 @@ export class NodeCommandHandlers { ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in updating nodes') + }, 'Error in updating nodes', lease) await action(argv, this) return true } - async delete (argv) { + async delete (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DELETE_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - ...this.deletePrepareTaskList(argv), + ...this.deletePrepareTaskList(argv, lease), ...this.deleteSubmitTransactionsTaskList(argv), ...this.deleteExecuteTaskList(argv) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in deleting nodes') + }, 'Error in deleting nodes', lease) await action(argv, this) return true } - async deletePrepare (argv) { + async deletePrepare (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DELETE_PREPARE_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - ...this.deletePrepareTaskList(argv), + ...this.deletePrepareTaskList(argv, lease), this.tasks.saveContextData(argv, NodeCommandHandlers.DELETE_CONTEXT_FILE, helpers.deleteSaveContextParser) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in preparing to delete a node') + }, 'Error in preparing to delete a node', lease) await action(argv, this) return true } - async deleteSubmitTransactions (argv) { + async deleteSubmitTransactions (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DELETE_SUBMIT_TRANSACTIONS_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, updateConfigBuilder.bind(this)), + this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), this.tasks.loadContextData(argv, NodeCommandHandlers.DELETE_CONTEXT_FILE, helpers.deleteLoadContextParser), ...this.deleteSubmitTransactionsTaskList(argv) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in deleting a node') + }, 'Error in deleting a node', lease) await action(argv, this) return true } - async deleteExecute (argv) { + async deleteExecute (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DELETE_EXECUTE_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, deleteConfigBuilder.bind(this)), + this.tasks.initialize(argv, deleteConfigBuilder.bind(this), lease), this.tasks.loadContextData(argv, NodeCommandHandlers.DELETE_CONTEXT_FILE, helpers.deleteLoadContextParser), ...this.deleteExecuteTaskList(argv) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in deleting a node') + }, 'Error in deleting a node', lease) await action(argv, this) return true } - async add (argv) { + async add (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - ...this.addPrepareTasks(argv), + ...this.addPrepareTasks(argv, lease), ...this.addSubmitTransactionsTasks(argv), ...this.addExecuteTasks(argv) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in adding node') + }, 'Error in adding node', lease) await action(argv, this) return true } - async addPrepare (argv) { + async addPrepare (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_PREPARE_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - ...this.addPrepareTasks(argv), + ...this.addPrepareTasks(argv, lease), this.tasks.saveContextData(argv, NodeCommandHandlers.ADD_CONTEXT_FILE, helpers.addSaveContextParser), ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in preparing node') + }, 'Error in preparing node', lease) await action(argv, this) return true } - async addSubmitTransactions (argv) { + async addSubmitTransactions (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_SUBMIT_TRANSACTIONS_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, addConfigBuilder.bind(this)), + this.tasks.initialize(argv, addConfigBuilder.bind(this), lease), this.tasks.loadContextData(argv, NodeCommandHandlers.ADD_CONTEXT_FILE, helpers.addLoadContextParser), ...this.addSubmitTransactionsTasks(argv) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, '`Error in submitting transactions to node') + }, '`Error in submitting transactions to node', lease) await action(argv, this) return true } - async addExecute (argv) { + async addExecute (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.ADD_EXECUTE_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, addConfigBuilder.bind(this)), + this.tasks.initialize(argv, addConfigBuilder.bind(this), lease), this.tasks.identifyExistingNodes(), this.tasks.loadContextData(argv, NodeCommandHandlers.ADD_CONTEXT_FILE, helpers.addLoadContextParser), ...this.addExecuteTasks(argv) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in adding node') + }, 'Error in adding node', lease) await action(argv, this) return true } - async logs (argv) { + async logs (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.LOGS_FLAGS) const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, logsConfigBuilder.bind(this)), + this.tasks.initialize(argv, logsConfigBuilder.bind(this), null), this.tasks.getNodeLogsAndConfigs() ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in downloading log from nodes') + }, 'Error in downloading log from nodes', null) await action(argv, this) return true } - async refresh (argv) { + async refresh (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.REFRESH_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, refreshConfigBuilder.bind(this)), + this.tasks.initialize(argv, refreshConfigBuilder.bind(this), lease), this.tasks.identifyNetworkPods(), this.tasks.dumpNetworkNodesSaveState(), this.tasks.fetchPlatformSoftware('nodeAliases'), @@ -431,47 +456,56 @@ export class NodeCommandHandlers { ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in refreshing nodes') + }, 'Error in refreshing nodes', lease) await action(argv, this) return true } - async keys (argv) { + async keys (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.KEYS_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, keysConfigBuilder.bind(this)), + this.tasks.initialize(argv, keysConfigBuilder.bind(this), lease), this.tasks.generateGossipKeys(), this.tasks.generateGrpcTlsKeys(), this.tasks.finalize() ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error generating keys') + }, 'Error generating keys', lease) await action(argv, this) return true } - async stop (argv) { + async stop (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.STOP_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, stopConfigBuilder.bind(this)), + this.tasks.initialize(argv, stopConfigBuilder.bind(this), lease), this.tasks.identifyNetworkPods(), this.tasks.stopNodes() ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error stopping node') + }, 'Error stopping node', lease) await action(argv, this) return true } - async start (argv) { + async start (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.START_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, startConfigBuilder.bind(this)), + this.tasks.initialize(argv, startConfigBuilder.bind(this), lease), this.tasks.identifyExistingNodes(), this.tasks.startNodes('nodeAliases'), this.tasks.enablePortForwarding(), @@ -481,23 +515,26 @@ export class NodeCommandHandlers { ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error starting node') + }, 'Error starting node', lease) await action(argv, this) return true } - async setup (argv) { + async setup (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.SETUP_FLAGS) + + const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, setupConfigBuilder.bind(this)), + this.tasks.initialize(argv, setupConfigBuilder.bind(this), lease), this.tasks.identifyNetworkPods(), this.tasks.fetchPlatformSoftware('nodeAliases'), this.tasks.setupNetworkNodes('nodeAliases') ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in setting up nodes') + }, 'Error in setting up nodes', lease) await action(argv, this) return true diff --git a/src/commands/node/index.ts b/src/commands/node/index.ts index 4b526b284..a61751bd6 100644 --- a/src/commands/node/index.ts +++ b/src/commands/node/index.ts @@ -16,34 +16,25 @@ */ import { IllegalArgumentError } from '../../core/errors.ts' -import { - type KeyManager, - type PackageDownloader, - type PlatformInstaller, type ProfileManager, - YargsCommand -} from '../../core/index.ts' +import { type AccountManager, YargsCommand } from '../../core/index.ts' import { BaseCommand } from './../base.ts' import { NodeCommandTasks } from './tasks.ts' import * as NodeFlags from './flags.ts' import { NodeCommandHandlers } from './handlers.ts' -import type { AccountManager } from '../../core/account_manager.ts' +import type { Opts } from '../../types/index.ts' /** * Defines the core functionalities of 'node' command */ export class NodeCommand extends BaseCommand { - private readonly downloader: PackageDownloader - private readonly platformInstaller: PlatformInstaller - private readonly keyManager: KeyManager private readonly accountManager: AccountManager - private readonly profileManager: ProfileManager public readonly tasks: NodeCommandTasks public readonly handlers: NodeCommandHandlers public _portForwards: any - constructor (opts) { + constructor (opts: Opts) { super(opts) if (!opts || !opts.downloader) throw new IllegalArgumentError('An instance of core/PackageDownloader is required', opts.downloader) @@ -52,11 +43,7 @@ export class NodeCommand extends BaseCommand { if (!opts || !opts.accountManager) throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager) if (!opts || !opts.profileManager) throw new IllegalArgumentError('An instance of ProfileManager is required', opts.profileManager) - this.downloader = opts.downloader - this.platformInstaller = opts.platformInstaller - this.keyManager = opts.keyManager this.accountManager = opts.accountManager - this.profileManager = opts.profileManager this._portForwards = [] this.tasks = new NodeCommandTasks({ @@ -78,7 +65,8 @@ export class NodeCommand extends BaseCommand { logger: opts.logger, k8: opts.k8, tasks: this.tasks, - parent: this + parent: this, + leaseManager: opts.leaseManager }) } @@ -86,7 +74,6 @@ export class NodeCommand extends BaseCommand { * stops and closes the port forwards * - calls the accountManager.close() * - for all portForwards, calls k8.stopPortForward(srv) - * @returns {Promise} */ async close () { await this.accountManager.close() @@ -99,17 +86,12 @@ export class NodeCommand extends BaseCommand { this._portForwards = [] } - // Command Definition - /** - * Return Yargs command definition for 'node' command - * @returns {{command: string, desc: string, builder: Function}} - */ getCommandDefinition () { const nodeCmd = this return { command: 'node', desc: 'Manage Hedera platform node in solo network', - builder: yargs => { + builder: (yargs: any) => { return yargs .command(new YargsCommand({ command: 'setup', diff --git a/src/commands/node/tasks.ts b/src/commands/node/tasks.ts index 898789504..4358f020f 100644 --- a/src/commands/node/tasks.ts +++ b/src/commands/node/tasks.ts @@ -25,7 +25,8 @@ import { type ProfileManager, Task, Templates, - Zippy + Zippy, + type AccountManager } from '../../core/index.ts' import { DEFAULT_NETWORK_NODE_NAME, @@ -62,12 +63,14 @@ import { import chalk from 'chalk' import * as flags from '../flags.ts' import { type SoloLogger } from '../../core/logging.ts' -import { type AccountManager } from '../../core/account_manager.ts' import type { Listr, ListrTaskWrapper } from 'listr2' import { type NodeAlias, type NodeAliases, type PodName } from '../../types/aliases.ts' import { NodeStatusCodes, NodeStatusEnums } from '../../core/enumerations.ts' import * as x509 from '@peculiar/x509' -import { type NodeCommand } from './index.js' +import { type NodeCommand } from './index.ts' +import type { NodeDeleteConfigClass, NodeRefreshConfigClass, NodeUpdateConfigClass } from './configs.ts' +import type { NodeAddConfigClass } from './configs.ts' +import type { LeaseWrapper } from '../../core/lease_wrapper.ts' export class NodeCommandTasks { private readonly accountManager: AccountManager @@ -84,7 +87,8 @@ export class NodeCommandTasks { constructor (opts: { logger: SoloLogger; accountManager: AccountManager; configManager: ConfigManager, k8: K8, platformInstaller: PlatformInstaller, keyManager: KeyManager, profileManager: ProfileManager, - chartManager: ChartManager, parent: NodeCommand}) { + chartManager: ChartManager, parent: NodeCommand} + ) { if (!opts || !opts.accountManager) throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager as any) if (!opts || !opts.configManager) throw new Error('An instance of core/ConfigManager is required') if (!opts || !opts.logger) throw new Error('An instance of core/Logger is required') @@ -178,20 +182,19 @@ export class NodeCommandTasks { this.logger.debug('no need to fetch, use local build jar files') - /** @type {Map} */ - const buildPathMap = new Map() - let defaultDataLibBuildPath + const buildPathMap = new Map() + let defaultDataLibBuildPath: string const parameterPairs = localBuildPath.split(',') for (const parameterPair of parameterPairs) { if (parameterPair.includes('=')) { const [nodeAlias, localDataLibBuildPath] = parameterPair.split('=') - buildPathMap.set(nodeAlias, localDataLibBuildPath) + buildPathMap.set(nodeAlias as NodeAlias, localDataLibBuildPath) } else { defaultDataLibBuildPath = parameterPair } } - let localDataLibBuildPath + let localDataLibBuildPath: string for (const nodeAlias of nodeAliases) { const podName = podNames[nodeAlias] if (buildPathMap.has(nodeAlias)) { @@ -231,7 +234,8 @@ export class NodeCommandTasks { } _fetchPlatformSoftware (nodeAliases: NodeAliases, podNames: Record, releaseTag: string, - task: ListrTaskWrapper, platformInstaller: PlatformInstaller) { + task: ListrTaskWrapper, platformInstaller: PlatformInstaller + ) { const subTasks = [] for (const nodeAlias of nodeAliases) { const podName = podNames[nodeAlias] @@ -258,7 +262,7 @@ export class NodeCommandTasks { const reminder = ('debugNodeAlias' in ctx.config && ctx.config.debugNodeAlias === nodeAlias) ? 'Please attach JVM debugger now.' : '' const title = `Check network pod: ${chalk.yellow(nodeAlias)} ${chalk.red(reminder)}` - const subTask = async (ctx, task) => { + const subTask = async (ctx: any, task: ListrTaskWrapper) => { ctx.config.podNames[nodeAlias] = await this._checkNetworkNodeActiveness(namespace, nodeAlias, task, title, i, status) } @@ -274,8 +278,9 @@ export class NodeCommandTasks { } async _checkNetworkNodeActiveness (namespace: string, nodeAlias: NodeAlias, task: ListrTaskWrapper, - title: string, index: number, status = NodeStatusCodes.ACTIVE, - maxAttempts = 120, delay = 1_000, timeout = 1_000) { + title: string, index: number, status = NodeStatusCodes.ACTIVE, + maxAttempts = 120, delay = 1_000, timeout = 1_000 + ) { nodeAlias = nodeAlias.trim() as NodeAlias const podName = Templates.renderNetworkPodName(nodeAlias) const podPort = 9_999 @@ -348,9 +353,7 @@ export class NodeCommandTasks { return podName } - /** - * Return task for check if node proxies are ready - */ + /** Return task for check if node proxies are ready */ _checkNodesProxiesTask (ctx: any, task: ListrTaskWrapper, nodeAliases: NodeAliases) { const subTasks = [] for (const nodeAlias of nodeAliases) { @@ -376,7 +379,7 @@ export class NodeCommandTasks { * When generating a single key the alias in config.nodeAlias is used */ _generateGossipKeys (generateMultiple: boolean) { - return new Task('Generate gossip keys', (ctx, task) => { + return new Task('Generate gossip keys', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config const nodeAliases = generateMultiple ? config.nodeAliases : [config.nodeAlias] const subTasks = this.keyManager.taskGenerateGossipKeys(nodeAliases, config.keysDir, config.curDate) @@ -388,16 +391,15 @@ export class NodeCommandTasks { timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION } }) - }, (ctx) => !ctx.config.generateGossipKeys) + }, (ctx: any) => !ctx.config.generateGossipKeys) } /** - * * When generating multiple all aliases are read from config.nodeAliases, * When generating a single key the alias in config.nodeAlias is used */ _generateGrpcTlsKeys (generateMultiple: boolean) { - return new Task('Generate gRPC TLS Keys', (ctx, task) => { + return new Task('Generate gRPC TLS Keys', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config const nodeAliases = generateMultiple ? config.nodeAliases : [config.nodeAlias] const subTasks = this.keyManager.taskGenerateTLSKeys(nodeAliases, config.keysDir, config.curDate) @@ -409,10 +411,10 @@ export class NodeCommandTasks { timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION } }) - }, (ctx) => !ctx.config.generateTlsKeys) + }, (ctx: any) => !ctx.config.generateTlsKeys) } - _loadPermCertificate (certFullPath) { + _loadPermCertificate (certFullPath: string) { const certPem = fs.readFileSync(certFullPath).toString() const decodedDers = x509.PemConverter.decode(certPem) if (!decodedDers || decodedDers.length === 0) { @@ -439,7 +441,7 @@ export class NodeCommandTasks { this.logger.debug(`Account ${accountId} balance: ${balance.hbars}`) // Create the transaction - const transaction = await new AccountUpdateTransaction() + const transaction = new AccountUpdateTransaction() .setAccountId(accountId) .setStakedNodeId(Templates.nodeIdFromNodeAlias(nodeAlias) - 1) .freezeWith(client) @@ -583,9 +585,7 @@ export class NodeCommandTasks { } taskCheckNetworkNodePods (ctx: any, task: ListrTaskWrapper, nodeAliases: NodeAliases): Listr { - if (!ctx.config) { - ctx.config = {} - } + if (!ctx.config) ctx.config = {} ctx.config.podNames = {} @@ -644,8 +644,8 @@ export class NodeCommandTasks { }) } - fetchPlatformSoftware (aliasesField) { - return new Task('Fetch platform software into network nodes', (ctx, task) => { + fetchPlatformSoftware (aliasesField: string) { + return new Task('Fetch platform software into network nodes', (ctx: any, task: ListrTaskWrapper) => { const { podNames, releaseTag, localBuildPath } = ctx.config if (localBuildPath !== '') { @@ -657,7 +657,7 @@ export class NodeCommandTasks { } populateServiceMap () { - return new Task('Populate serviceMap', async (ctx, task) => { + return new Task('Populate serviceMap', async (ctx: any, task: ListrTaskWrapper) => { ctx.config.serviceMap = await this.accountManager.getNodeServiceMap( ctx.config.namespace) ctx.config.podNames[ctx.config.nodeAlias] = ctx.config.serviceMap.get(ctx.config.nodeAlias).nodePodName @@ -665,7 +665,7 @@ export class NodeCommandTasks { } setupNetworkNodes (nodeAliasesProperty: string) { - return new Task('Setup network nodes', (ctx, task) => { + return new Task('Setup network nodes', (ctx: any, task: ListrTaskWrapper) => { const subTasks = [] for (const nodeAlias of ctx.config[nodeAliasesProperty]) { const podName = ctx.config.podNames[nodeAlias] @@ -684,15 +684,15 @@ export class NodeCommandTasks { }) } - prepareStagingDirectory (nodeAliasesProperty) { - return new Task('Prepare staging directory', (ctx, task) => { + prepareStagingDirectory (nodeAliasesProperty: any) { + return new Task('Prepare staging directory', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config const nodeAliases = config[nodeAliasesProperty] const subTasks = [ { title: 'Copy Gossip keys to staging', task: async () => { - await this.keyManager.copyGossipKeysToStaging(config.keysDir, config.stagingKeysDir, nodeAliases) + this.keyManager.copyGossipKeysToStaging(config.keysDir, config.stagingKeysDir, nodeAliases) } }, { @@ -700,7 +700,7 @@ export class NodeCommandTasks { task: async () => { for (const nodeAlias of nodeAliases) { const tlsKeyFiles = this.keyManager.prepareTLSKeyFilePaths(nodeAlias, config.keysDir) - await this.keyManager.copyNodeKeysToStaging(tlsKeyFiles, config.stagingKeysDir) + this.keyManager.copyNodeKeysToStaging(tlsKeyFiles, config.stagingKeysDir) } } } @@ -712,8 +712,8 @@ export class NodeCommandTasks { }) } - startNodes (nodeAliasesProperty) { - return new Task('Starting nodes', (ctx, task) => { + startNodes (nodeAliasesProperty: string) { + return new Task('Starting nodes', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config const nodeAliases = config[nodeAliasesProperty] @@ -742,35 +742,35 @@ export class NodeCommandTasks { } enablePortForwarding () { - return new Task('Enable port forwarding for JVM debugger', async (ctx, task) => { + return new Task('Enable port forwarding for JVM debugger', async (ctx: any, task: ListrTaskWrapper) => { const podName = `network-${ctx.config.debugNodeAlias}-0` as PodName this.logger.debug(`Enable port forwarding for JVM debugger on pod ${podName}`) await this.k8.portForward(podName, constants.JVM_DEBUG_PORT, constants.JVM_DEBUG_PORT) - }, (ctx) => !ctx.config.debugNodeAlias) + }, (ctx: any) => !ctx.config.debugNodeAlias) } - checkAllNodesAreActive (nodeAliasesProperty) { - return new Task('Check all nodes are ACTIVE', (ctx, task) => { + checkAllNodesAreActive (nodeAliasesProperty: string) { + return new Task('Check all nodes are ACTIVE', (ctx: any, task: ListrTaskWrapper) => { return this._checkNodeActivenessTask(ctx, task, ctx.config[nodeAliasesProperty]) }) } - checkAllNodesAreFrozen (nodeAliasesProperty) { - return new Task('Check all nodes are ACTIVE', (ctx, task) => { + checkAllNodesAreFrozen (nodeAliasesProperty: string) { + return new Task('Check all nodes are ACTIVE', (ctx: any, task: ListrTaskWrapper) => { return this._checkNodeActivenessTask(ctx, task, ctx.config[nodeAliasesProperty], NodeStatusCodes.FREEZE_COMPLETE) }) } checkNodeProxiesAreActive () { - return new Task('Check node proxies are ACTIVE', (ctx, task) => { + return new Task('Check node proxies are ACTIVE', (ctx: any, task: ListrTaskWrapper) => { // this is more reliable than checking the nodes logs for ACTIVE, as the // logs will have a lot of white noise from being behind return this._checkNodesProxiesTask(ctx, task, ctx.config.nodeAliases) - }, async (ctx) => ctx.config.app !== '' && ctx.config.app !== constants.HEDERA_APP_NAME) + }, async (ctx: any) => ctx.config.app !== '' && ctx.config.app !== constants.HEDERA_APP_NAME) } checkAllNodeProxiesAreActive () { - return new Task('Check all node proxies are ACTIVE', (ctx, task) => { + return new Task('Check all node proxies are ACTIVE', (ctx: any, task: ListrTaskWrapper) => { // this is more reliable than checking the nodes logs for ACTIVE, as the // logs will have a lot of white noise from being behind return this._checkNodesProxiesTask(ctx, task, ctx.config.allNodeAliases) @@ -779,10 +779,10 @@ export class NodeCommandTasks { // Update account manager and transfer hbar for staking purpose triggerStakeWeightCalculate () { - return new Task('Trigger stake weight calculate', async (ctx, task) => { + return new Task('Trigger stake weight calculate', async (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config this.logger.info('sleep 60 seconds for the handler to be able to trigger the network node stake weight recalculate') - await sleep(60000) + await sleep(60 * SECONDS) const accountMap = getNodeAccountMap(config.allNodeAliases) if (config.newAccountNumber) { @@ -803,7 +803,8 @@ export class NodeCommandTasks { } addNodeStakes () { - return new Task('Add node stakes', (ctx, task) => { + // @ts-ignore + return new Task('Add node stakes', (ctx: any, task: ListrTaskWrapper) => { if (ctx.config.app === '' || ctx.config.app === constants.HEDERA_APP_NAME) { const subTasks = [] const accountMap = getNodeAccountMap(ctx.config.nodeAliases) @@ -827,13 +828,13 @@ export class NodeCommandTasks { } stakeNewNode () { - return new Task('Stake new node', async (ctx, task) => { + return new Task('Stake new node', async (ctx: any, task: ListrTaskWrapper) => { await this._addStake(ctx.config.namespace, ctx.newNode.accountId, ctx.config.nodeAlias) }) } stopNodes () { - return new Task('Stopping nodes', (ctx, task) => { + return new Task('Stopping nodes', (ctx: any, task: ListrTaskWrapper) => { const subTasks = [] for (const nodeAlias of ctx.config.nodeAliases) { const podName = ctx.config.podNames[nodeAlias] @@ -855,7 +856,7 @@ export class NodeCommandTasks { } finalize () { - return new Task('Finalize', (ctx, task) => { + return new Task('Finalize', (ctx: any, task: ListrTaskWrapper) => { // reset flags so that keys are not regenerated later this.configManager.setFlag(flags.generateGossipKeys, false) this.configManager.setFlag(flags.generateTlsKeys, false) @@ -863,8 +864,8 @@ export class NodeCommandTasks { } dumpNetworkNodesSaveState () { - return new Task('Dump network nodes saved state', (ctx, task) => { - const config = /** @type {NodeRefreshConfigClass} **/ ctx.config + return new Task('Dump network nodes saved state', (ctx: any, task: ListrTaskWrapper) => { + const config: NodeRefreshConfigClass = ctx.config const subTasks = [] for (const nodeAlias of config.nodeAliases) { const podName = config.podNames[nodeAlias] @@ -886,13 +887,13 @@ export class NodeCommandTasks { } getNodeLogsAndConfigs () { - return new Task('Get node logs and configs', async (ctx, task) => { + return new Task('Get node logs and configs', async (ctx: any, task: ListrTaskWrapper) => { await getNodeLogs(this.k8, ctx.config.namespace) }) } checkPVCsEnabled () { - return new Task('Check that PVCs are enabled', (ctx, task) => { + return new Task('Check that PVCs are enabled', (ctx: any, task: ListrTaskWrapper) => { if (!this.configManager.getFlag(flags.persistentVolumeClaims)) { throw new SoloError('PVCs are not enabled. Please enable PVCs before adding a node') } @@ -900,14 +901,14 @@ export class NodeCommandTasks { } determineNewNodeAccountNumber () { - return new Task('Determine new node account number', (ctx, task) => { - const config = /** @type {NodeAddConfigClass} **/ ctx.config + return new Task('Determine new node account number', (ctx: any, task: ListrTaskWrapper) => { + const config: NodeAddConfigClass = ctx.config const values = { hedera: { nodes: [] } } let maxNum = 0 let lastNodeAlias = DEFAULT_NETWORK_NODE_NAME - for (/** @type {NetworkNodeServices} **/ const networkNodeServices of config.serviceMap.values()) { + for (const networkNodeServices of config.serviceMap.values()) { values.hedera.nodes.push({ accountId: networkNodeServices.accountId, name: networkNodeServices.nodeAlias @@ -929,8 +930,8 @@ export class NodeCommandTasks { accountId: `${constants.HEDERA_NODE_ACCOUNT_ID_START.realm}.${constants.HEDERA_NODE_ACCOUNT_ID_START.shard}.${++maxNum}`, name: lastNodeAlias } - config.nodeAlias = lastNodeAlias - config.allNodeAliases.push(lastNodeAlias) + config.nodeAlias = lastNodeAlias as NodeAlias + config.allNodeAliases.push(lastNodeAlias as NodeAlias) }) } @@ -951,7 +952,7 @@ export class NodeCommandTasks { } loadSigningKeyCertificate () { - return new Task('Load signing key certificate', (ctx, task) => { + return new Task('Load signing key certificate', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config const signingCertFile = Templates.renderGossipPemPublicKeyFile(config.nodeAlias) const signingCertFullPath = path.join(config.keysDir, signingCertFile) @@ -960,7 +961,7 @@ export class NodeCommandTasks { } computeMTLSCertificateHash () { - return new Task('Compute mTLS certificate hash', (ctx, task) => { + return new Task('Compute mTLS certificate hash', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config const tlsCertFile = Templates.renderTLSPemPublicKeyFile(config.nodeAlias) const tlsCertFullPath = path.join(config.keysDir, tlsCertFile) @@ -970,7 +971,7 @@ export class NodeCommandTasks { } prepareGossipEndpoints () { - return new Task('Prepare gossip endpoints', (ctx, task) => { + return new Task('Prepare gossip endpoints', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config let endpoints = [] if (!config.gossipEndpoints) { @@ -991,13 +992,13 @@ export class NodeCommandTasks { } refreshNodeList () { - return new Task('Refresh node alias list', (ctx, task) => { - ctx.config.allNodeAliases = ctx.config.existingNodeAliases.filter(nodeAlias => nodeAlias !== ctx.config.nodeAlias) + return new Task('Refresh node alias list', (ctx: any, task: ListrTaskWrapper) => { + ctx.config.allNodeAliases = ctx.config.existingNodeAliases.filter((nodeAlias: NodeAlias) => nodeAlias !== ctx.config.nodeAlias) }) } prepareGrpcServiceEndpoints () { - return new Task('Prepare grpc service endpoints', (ctx, task) => { + return new Task('Prepare grpc service endpoints', (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config let endpoints = [] @@ -1018,16 +1019,15 @@ export class NodeCommandTasks { } sendNodeUpdateTransaction () { - return new Task('Send node update transaction', async (ctx, task) => { - const config = /** @type {NodeUpdateConfigClass} **/ ctx.config + return new Task('Send node update transaction', async (ctx: any, task: ListrTaskWrapper) => { + const config: NodeUpdateConfigClass = ctx.config const nodeId = Templates.nodeIdFromNodeAlias(config.nodeAlias) - 1 this.logger.info(`nodeId: ${nodeId}`) this.logger.info(`config.newAccountNumber: ${config.newAccountNumber}`) try { - const nodeUpdateTx = await new NodeUpdateTransaction() - .setNodeId(nodeId) + const nodeUpdateTx = new NodeUpdateTransaction().setNodeId(nodeId) if (config.tlsPublicKey && config.tlsPrivateKey) { this.logger.info(`config.tlsPublicKey: ${config.tlsPublicKey}`) @@ -1080,7 +1080,7 @@ export class NodeCommandTasks { } copyNodeKeysToSecrets () { - return new Task('Copy node keys to secrets', (ctx, task) => { + return new Task('Copy node keys to secrets', (ctx: any, task: ListrTaskWrapper) => { const subTasks = this.platformInstaller.copyNodeKeys(ctx.config.stagingDir, ctx.config.allNodeAliases) // set up the sub-tasks for copying node keys to staging directory @@ -1092,7 +1092,7 @@ export class NodeCommandTasks { } updateChartWithConfigMap (title: string, skip: Function | boolean = false) { - return new Task(title, async (ctx, task) => { + return new Task(title, async (ctx: any, task: ListrTaskWrapper) => { // Prepare parameter and update the network node chart const config = ctx.config @@ -1136,8 +1136,8 @@ export class NodeCommandTasks { }, skip) } - saveContextData (argv, targetFile, parser) { - return new Task('Save context data', (ctx, task) => { + saveContextData (argv: any, targetFile: string, parser: any) { + return new Task('Save context data', (ctx: any, task: ListrTaskWrapper) => { const outputDir = argv[flags.outputDir.name] if (!outputDir) { throw new SoloError(`Path to export context data not specified. Please set a value for --${flags.outputDir.name}`) @@ -1151,8 +1151,8 @@ export class NodeCommandTasks { }) } - loadContextData (argv, targetFile, parser) { - return new Task('Load context data', (ctx, task) => { + loadContextData (argv: any, targetFile: string, parser: any) { + return new Task('Load context data', (ctx: any, task: ListrTaskWrapper) => { const inputDir = argv[flags.inputDir.name] if (!inputDir) { throw new SoloError(`Path to context data not specified. Please set a value for --${flags.inputDir.name}`) @@ -1164,29 +1164,29 @@ export class NodeCommandTasks { } killNodes () { - return new Task('Kill nodes', async (ctx, task) => { + return new Task('Kill nodes', async (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config - for (const /** @type {NetworkNodeServices} **/ service of config.serviceMap.values()) { + for (const service of config.serviceMap.values()) { await this.k8.kubeClient.deleteNamespacedPod(service.nodePodName, config.namespace, undefined, undefined, 1) } }) } killNodesAndUpdateConfigMap () { - return new Task('Kill nodes to pick up updated configMaps', async (ctx, task) => { + return new Task('Kill nodes to pick up updated configMaps', async (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config // the updated node will have a new pod ID if its account ID changed which is a label - config.serviceMap = await this.accountManager.getNodeServiceMap( - config.namespace) - for (const /** @type {NetworkNodeServices} **/ service of config.serviceMap.values()) { + config.serviceMap = await this.accountManager.getNodeServiceMap(config.namespace) + + for (const service of config.serviceMap.values()) { await this.k8.kubeClient.deleteNamespacedPod(service.nodePodName, config.namespace, undefined, undefined, 1) } this.logger.info('sleep for 15 seconds to give time for pods to finish terminating') - await sleep(15000) + await sleep(15 * SECONDS) // again, the pod names will change after the pods are killed - config.serviceMap = await this.accountManager.getNodeServiceMap( - config.namespace) + config.serviceMap = await this.accountManager.getNodeServiceMap(config.namespace) + config.podNames = {} for (const service of config.serviceMap.values()) { config.podNames[service.nodeAlias] = service.nodePodName @@ -1195,8 +1195,8 @@ export class NodeCommandTasks { } checkNodePodsAreRunning () { - return new Task('Check node pods are running', (ctx, task) => { - const config = /** @type {NodeUpdateConfigClass} **/ ctx.config + return new Task('Check node pods are running', (ctx: any, task: ListrTaskWrapper) => { + const config: NodeUpdateConfigClass = ctx.config const subTasks = [] for (const nodeAlias of config.allNodeAliases) { subTasks.push({ @@ -1219,8 +1219,8 @@ export class NodeCommandTasks { }) } - sleep (title, milliseconds) { - return new Task(title, async (ctx, task) => { + sleep (title: string, milliseconds: number) { + return new Task(title, async (ctx: any, task: ListrTaskWrapper) => { await sleep(milliseconds) }) } @@ -1238,7 +1238,7 @@ export class NodeCommandTasks { } uploadStateToNewNode () { - return new Task('Upload last saved state to new network node', async (ctx, task) => { + return new Task('Upload last saved state to new network node', async (ctx: any, task: ListrTaskWrapper) => { const config = ctx.config const newNodeFullyQualifiedPodName = Templates.renderNetworkPodName(config.nodeAlias) const nodeId = Templates.nodeIdFromNodeAlias(config.nodeAlias) @@ -1252,23 +1252,24 @@ export class NodeCommandTasks { } sendNodeDeleteTransaction () { - return new Task('Send node delete transaction', async (ctx, task) => { - const config = /** @type {NodeDeleteConfigClass} **/ ctx.config + return new Task('Send node delete transaction', async (ctx: any, task: ListrTaskWrapper) => { + const config: NodeDeleteConfigClass = ctx.config try { const accountMap = getNodeAccountMap(config.existingNodeAliases) const deleteAccountId = accountMap.get(config.nodeAlias) this.logger.debug(`Deleting node: ${config.nodeAlias} with account: ${deleteAccountId}`) const nodeId = Templates.nodeIdFromNodeAlias(config.nodeAlias) - 1 - const nodeDeleteTx = await new NodeDeleteTransaction() + const nodeDeleteTx = new NodeDeleteTransaction() .setNodeId(nodeId) .freezeWith(config.nodeClient) const signedTx = await nodeDeleteTx.sign(config.adminKey) const txResp = await signedTx.execute(config.nodeClient) const nodeUpdateReceipt = await txResp.getReceipt(config.nodeClient) + this.logger.debug(`NodeUpdateReceipt: ${nodeUpdateReceipt.toString()}`) - } catch (e) { + } catch (e: Error | any) { this.logger.error(`Error deleting node from network: ${e.message}`, e) throw new SoloError(`Error deleting node from network: ${e.message}`, e) } @@ -1276,11 +1277,11 @@ export class NodeCommandTasks { } sendNodeCreateTransaction () { - return new Task('Send node create transaction', async (ctx, task) => { - const config = /** @type {NodeAddConfigClass} **/ ctx.config + return new Task('Send node create transaction', async (ctx: any, task: ListrTaskWrapper) => { + const config: NodeAddConfigClass = ctx.config try { - const nodeCreateTx = await new NodeCreateTransaction() + const nodeCreateTx = new NodeCreateTransaction() .setAccountId(ctx.newNode.accountId) .setGossipEndpoints(ctx.gossipEndpoints) .setServiceEndpoints(ctx.grpcServiceEndpoints) @@ -1292,7 +1293,7 @@ export class NodeCommandTasks { const txResp = await signedTx.execute(config.nodeClient) const nodeCreateReceipt = await txResp.getReceipt(config.nodeClient) this.logger.debug(`NodeCreateReceipt: ${nodeCreateReceipt.toString()}`) - } catch (e) { + } catch (e: Error | any) { this.logger.error(`Error adding node to network: ${e.message}`, e) throw new SoloError(`Error adding node to network: ${e.message}`, e) } @@ -1300,12 +1301,12 @@ export class NodeCommandTasks { } templateTask () { - return new Task('TEMPLATE', async (ctx, task) => { + return new Task('TEMPLATE', async (ctx: any, task: ListrTaskWrapper) => { }) } - initialize (argv: any, configInit: Function) { + initialize (argv: any, configInit: Function, lease: LeaseWrapper | null) { const { requiredFlags, requiredFlagsWithDisabledPrompt, optionalFlags } = argv const allRequiredFlags = [ ...requiredFlags, @@ -1318,6 +1319,7 @@ export class NodeCommandTasks { ...optionalFlags ] + // @ts-ignore return new Task('Initialize', async (ctx: any, task: ListrTaskWrapper) => { if (argv[flags.devMode.name]) { this.logger.setDevMode(true) @@ -1348,6 +1350,8 @@ export class NodeCommandTasks { } this.logger.debug('Initialized config', { config }) + + if (lease) return lease.buildAcquireTask(task) }) } } diff --git a/src/commands/relay.ts b/src/commands/relay.ts index 80b40e439..541049969 100644 --- a/src/commands/relay.ts +++ b/src/commands/relay.ts @@ -17,13 +17,12 @@ import { Listr } from 'listr2' import { SoloError, MissingArgumentError } from '../core/errors.ts' import * as helpers from '../core/helpers.ts' -import type { ProfileManager } from '../core/index.ts' +import type { ProfileManager, AccountManager } from '../core/index.ts' import { constants } from '../core/index.ts' import { BaseCommand } from './base.ts' import * as flags from './flags.ts' import * as prompts from './prompts.ts' import { getNodeAccountMap } from '../core/helpers.ts' -import type { AccountManager } from '../core/account_manager.ts' import { type NodeAliases } from '../types/aliases.ts' import { type Opts } from '../types/index.ts' @@ -158,6 +157,7 @@ export class RelayCommand extends BaseCommand { async deploy (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface RelayDeployConfigClass { chainId: string @@ -203,6 +203,8 @@ export class RelayCommand extends BaseCommand { ctx.config.isChartInstalled = await self.chartManager.isChartInstalled(ctx.config.namespace, ctx.config.releaseName) self.logger.debug('Initialized config', { config: ctx.config }) + + return lease.buildAcquireTask(task) } }, { @@ -261,6 +263,8 @@ export class RelayCommand extends BaseCommand { await tasks.run() } catch (e: Error | any) { throw new SoloError('Error installing relays', e) + } finally { + await lease.release() } return true @@ -268,6 +272,7 @@ export class RelayCommand extends BaseCommand { async destroy (argv: any) { const self = this + const lease = self.leaseManager.instantiateLease() interface RelayDestroyConfigClass { chartDirectory: string @@ -302,6 +307,8 @@ export class RelayCommand extends BaseCommand { ctx.config.isChartInstalled = await this.chartManager.isChartInstalled(ctx.config.namespace, ctx.config.releaseName) self.logger.debug('Initialized config', { config: ctx.config }) + + return lease.buildAcquireTask(task) } }, { @@ -327,6 +334,8 @@ export class RelayCommand extends BaseCommand { await tasks.run() } catch (e: Error | any) { throw new SoloError('Error uninstalling relays', e) + } finally { + await lease.release() } return true diff --git a/src/core/account_manager.ts b/src/core/account_manager.ts index 14958075a..62eecaa83 100644 --- a/src/core/account_manager.ts +++ b/src/core/account_manager.ts @@ -160,7 +160,7 @@ export class AccountManager { /** * loads and initializes the Node Client - * @param namespace the namespace of the network + * @param namespace - the namespace of the network */ async refreshNodeClient (namespace: string) { await this.close() diff --git a/src/core/constants.ts b/src/core/constants.ts index 94c62022f..faae35a86 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -19,8 +19,10 @@ import { AccountId, FileId } from '@hashgraph/sdk' import { color, type ListrLogger, PRESET_TIMER } from 'listr2' import path, { dirname, normalize } from 'path' import { fileURLToPath } from 'url' +import os from 'node:os' export const ROOT_DIR = path.join(dirname(fileURLToPath(import.meta.url)), '..', '..') +export const OS_USERNAME = os.userInfo().username // -------------------- solo related constants --------------------------------------------------------------------- export const SOLO_HOME_DIR = process.env.SOLO_HOME || path.join(process.env.HOME as string, '.solo') @@ -154,3 +156,7 @@ export const JVM_DEBUG_PORT = 5005 export const SECONDS = 1000 export const MINUTES = 60 * SECONDS + +export const LEASE_AQUIRE_RETRY_TIMEOUT = 20 * SECONDS +export const MAX_LEASE_ACQUIRE_ATTEMPTS = 10 +export const LEASE_RENEW_TIMEOUT = 10 * SECONDS diff --git a/src/core/helpers.ts b/src/core/helpers.ts index 974d0921c..407291061 100644 --- a/src/core/helpers.ts +++ b/src/core/helpers.ts @@ -32,6 +32,7 @@ import { type CommandFlag } from '../types/index.ts' import { type V1Pod } from '@kubernetes/client-node' import { type SoloLogger } from './logging.ts' import { type NodeCommandHandlers } from '../commands/node/handlers.ts' +import { type LeaseWrapper } from './lease_wrapper.ts' export function sleep (ms: number) { return new Promise((resolve) => { @@ -408,7 +409,7 @@ export function prepareEndpoints (endpointType: string, endpoints: string[], def return ret } -export function commandActionBuilder (actionTasks: any, options: any, errorString = 'Error') { +export function commandActionBuilder (actionTasks: any, options: any, errorString: string, lease: LeaseWrapper | null) { return async function (argv: any, commandDef: NodeCommandHandlers) { const tasks = new Listr([ ...actionTasks @@ -420,8 +421,9 @@ export function commandActionBuilder (actionTasks: any, options: any, errorStrin commandDef.parent.logger.error(`${errorString}: ${e.message}`, e) throw new SoloError(`${errorString}: ${e.message}`, e) } finally { - // @ts-ignore - await commandDef.close() + const promises = [commandDef.close()] + if (lease) promises.push(lease.release()) + await Promise.all(promises) } } } diff --git a/src/core/index.ts b/src/core/index.ts index 624980771..fa996eb89 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -29,6 +29,9 @@ import { ProfileManager } from './profile_manager.ts' import { YargsCommand } from './yargs_command.ts' import { Task } from './task.ts' import * as helpers from './helpers.ts' +import { DependencyManager } from './dependency_managers/index.ts' +import { AccountManager } from './account_manager.ts' +import { LeaseManager } from './lease_manager.ts' // Expose components from the core module export { @@ -46,5 +49,8 @@ export { KeyManager, ProfileManager, YargsCommand, - Task + Task, + DependencyManager, + AccountManager, + LeaseManager, } diff --git a/src/core/k8.ts b/src/core/k8.ts index 778f41453..029b89459 100644 --- a/src/core/k8.ts +++ b/src/core/k8.ts @@ -30,8 +30,9 @@ import * as stream from 'node:stream' import { type SoloLogger } from './logging.ts' import type * as WebSocket from 'ws' -import { type PodName } from '../types/aliases.ts' -import { type ExtendedNetServer, type LocalContextObject } from '../types/index.ts' +import type { PodName } from '../types/aliases.ts' +import type { ExtendedNetServer, LocalContextObject } from '../types/index.ts' +import type * as http from 'node:http' interface TDirectoryData {directory: boolean; owner: string; group: string; size: string; modifiedAt: string; name: string} @@ -42,9 +43,11 @@ interface TDirectoryData {directory: boolean; owner: string; group: string; size * For parallel execution, create separate instances by invoking clone() */ export class K8 { - static PodReadyCondition: Map = new Map().set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) + static PodReadyCondition = new Map() + .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) private kubeConfig!: k8s.KubeConfig kubeClient!: k8s.CoreV1Api + private coordinationApiClient: k8s.CoordinationV1Api constructor (private readonly configManager: ConfigManager, public readonly logger: SoloLogger) { if (!configManager) throw new MissingArgumentError('An instance of core/ConfigManager is required') @@ -81,6 +84,7 @@ export class K8 { } this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api) + this.coordinationApiClient = this.kubeConfig.makeApiClient(k8s.CoordinationV1Api) return this // to enable chaining } @@ -1145,6 +1149,69 @@ export class K8 { return resp.response.statusCode === 200.0 } + // --------------------------------------- LEASES --------------------------------------- // + async createNamespacedLease (namespace: string, leaseName: string, holderName: string) { + const lease = new k8s.V1Lease() + + const metadata = new k8s.V1ObjectMeta() + metadata.name = leaseName + metadata.namespace = namespace + lease.metadata = metadata + + const spec = new k8s.V1LeaseSpec() + spec.holderIdentity = holderName + spec.leaseDurationSeconds = 20 + spec.acquireTime = new k8s.V1MicroTime() + lease.spec = spec + + const { response, body } = await this.coordinationApiClient.createNamespacedLease(namespace, lease) + .catch(e => e) + + this._handleKubernetesClientError(response, body, 'Failed to create namespaced lease') + + return body as k8s.V1Lease + } + + async readNamespacedLease (leaseName: string, namespace: string) { + const { response, body } = await this.coordinationApiClient.readNamespacedLease(leaseName, namespace) + .catch(e => e) + + this._handleKubernetesClientError(response, body, 'Failed to read namespaced lease') + + return body as k8s.V1Lease + } + + async renewNamespaceLease (leaseName: string, namespace: string, lease: k8s.V1Lease) { + lease.spec.renewTime = new k8s.V1MicroTime() + + const { response, body } = await this.coordinationApiClient.replaceNamespacedLease(leaseName, namespace, lease) + .catch(e => e) + + this._handleKubernetesClientError(response, body, 'Failed to renew namespaced lease') + + return body as k8s.V1Lease + } + + async deleteNamespacedLease (name: string, namespace: string) { + const { response, body } = await this.coordinationApiClient.deleteNamespacedLease(name, namespace) + .catch(e => e) + + this._handleKubernetesClientError(response, body, 'Failed to delete namespaced lease') + + return body as k8s.V1Status + } + + private _handleKubernetesClientError (response: http.IncomingMessage, error: Error | any, errorMessage: string) { + const statusCode = +response.statusCode + + if (statusCode <= 202) return + errorMessage += `, statusCode: ${response.statusCode}` + this.logger.error(errorMessage, error) + + throw new SoloError(errorMessage, errorMessage, { statusCode: statusCode }) + } + + private _getNamespace () { const ns = this.configManager.getFlag(flags.namespace) if (!ns) throw new MissingArgumentError('namespace is not set') diff --git a/src/core/lease_manager.ts b/src/core/lease_manager.ts new file mode 100644 index 000000000..2c5d8b214 --- /dev/null +++ b/src/core/lease_manager.ts @@ -0,0 +1,157 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { MissingArgumentError, SoloError } from './errors.ts' +import { flags } from '../commands/index.ts' +import type { ConfigManager } from './config_manager.ts' +import type { K8 } from './k8.ts' +import type { SoloLogger } from './logging.ts' +import { LEASE_RENEW_TIMEOUT, LEASE_AQUIRE_RETRY_TIMEOUT, MAX_LEASE_ACQUIRE_ATTEMPTS, OS_USERNAME } from './constants.ts' +import type { ListrTaskWrapper } from 'listr2' +import chalk from 'chalk' +import { sleep } from './helpers.ts' +import { LeaseWrapper } from './lease_wrapper.ts' + +export class LeaseManager { + constructor ( + private readonly k8: K8, + private readonly logger: SoloLogger, + private readonly configManager: ConfigManager + ) { + if (!k8) throw new MissingArgumentError('an instance of core/K8 is required') + if (!logger) throw new MissingArgumentError('an instance of core/SoloLogger is required') + if (!configManager) throw new MissingArgumentError('an instance of core/ConfigManager is required') + } + + instantiateLease () { + return new LeaseWrapper(this) + } + + /** + * Acquires lease if is not already taken, automatically handles renews. + * + * @returns a callback function that releases the lease + */ + async acquireLease ( + task: ListrTaskWrapper, + title: string, + attempt: number | null = null + ): Promise<{ releaseLease: () => Promise }> { + const namespace = await this.getNamespace() + + //? In case namespace isn't yet created return an empty callback function + if (!namespace) { + task.title = `${title} - ${chalk.gray('namespace not created, skipping lease acquire')}` + + return { releaseLease: async () => {} } + } + + const username = OS_USERNAME + const leaseName = `${username}-lease` + + await this.acquireLeaseOrRetry(username, leaseName, namespace, task, title, attempt) + + const renewLeaseCallback = async () => { + try { + //? Get the latest most up-to-date lease to renew it + const lease = await this.k8.readNamespacedLease(leaseName, namespace) + + await this.k8.renewNamespaceLease(leaseName, namespace, lease) + } catch (error) { + throw new SoloError(`Failed to renew lease: ${error.message}`, error) + } + } + + //? Renew lease with the callback + const intervalId = setInterval(renewLeaseCallback, LEASE_RENEW_TIMEOUT) + + const releaseLeaseCallback = async () => { + //? Stop renewing the lease once release callback is called + clearInterval(intervalId) + + try { + await this.k8.deleteNamespacedLease(leaseName, namespace) + + this.logger.info(`Lease released by ${username}`) + } catch (e: Error | any) { + this.logger.error(`Failed to release lease: ${e.message}`) + } + } + + return { releaseLease: releaseLeaseCallback } + } + + private async acquireLeaseOrRetry ( + username: string, + leaseName: string, + namespace: string, + task: ListrTaskWrapper, + title: string, + attempt = 1, + maxAttempts = MAX_LEASE_ACQUIRE_ATTEMPTS, + ): Promise { + if (!attempt) attempt = 1 + + let exists = false + + try { + const lease = await this.k8.readNamespacedLease(leaseName, namespace) + + exists = !!lease + } catch (error) { + if (error.meta.statusCode !== 404) { + task.title = `${title} - ${chalk.red(`failed to acquire lease, unexpected server response ${error.meta.statusCode}!`)}` + + `, attempt: ${chalk.cyan(attempt.toString())}/${chalk.cyan(maxAttempts.toString())}` + + throw new SoloError(`Failed to acquire lease: ${error.message}`) + } + } + + //? In case the lease is already acquired retry after cooldown + if (exists) { + attempt++ + + if (attempt === maxAttempts) { + task.title = `${title} - ${chalk.red('failed to acquire lease, max attempts reached!')}` + + `, attempt: ${chalk.cyan(attempt.toString())}/${chalk.cyan(maxAttempts.toString())}` + + throw new SoloError(`Failed to acquire lease, max attempt reached ${attempt}`) + } + + this.logger.info(`Lease is already taken retrying in ${LEASE_AQUIRE_RETRY_TIMEOUT}`) + + task.title = `${title} - ${chalk.gray(`lease exists, attempting again in ${LEASE_AQUIRE_RETRY_TIMEOUT} seconds`)}` + + `, attempt: ${chalk.cyan(attempt.toString())}/${chalk.cyan(maxAttempts.toString())}` + + await sleep(LEASE_AQUIRE_RETRY_TIMEOUT) + + return this.acquireLeaseOrRetry(username, leaseName, namespace, task, title, attempt) + } + + await this.k8.createNamespacedLease(namespace, leaseName, username) + + task.title = `${title} - ${chalk.green('lease acquired successfully')}` + + `, attempt: ${chalk.cyan(attempt.toString())}/${chalk.cyan(maxAttempts.toString())}` + } + + private async getNamespace () { + const namespace = this.configManager.getFlag(flags.namespace) + if (!namespace) return null + + if (!await this.k8.hasNamespace(namespace)) return null + return namespace + } +} diff --git a/src/core/lease_wrapper.ts b/src/core/lease_wrapper.ts new file mode 100644 index 000000000..641c2d44a --- /dev/null +++ b/src/core/lease_wrapper.ts @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type { ListrTaskWrapper } from 'listr2' +import type { LeaseManager } from './lease_manager.ts' + +export class LeaseWrapper { + private releaseLease: () => Promise + + constructor (private readonly leaseManager: LeaseManager) {} + + private async acquireTask (task: ListrTaskWrapper, title: string, attempt: number | null = null) { + const self = this + + const { releaseLease } = await self.leaseManager.acquireLease(task, title, attempt) + self.releaseLease = releaseLease + } + + buildAcquireTask (task: ListrTaskWrapper) { + const self = this + + return task.newListr([ + { + title: 'Acquire lease', + task: async (_, task) => { + await self.acquireTask(task, 'Acquire lease') + } + } + ], { + concurrent: false, + rendererOptions: { + collapseSubtasks: false + } + }) + } + + async release () { + if (typeof this.releaseLease === 'function') { + await this.releaseLease() + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 240f455de..a18388ac5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,17 +21,11 @@ import { flags } from './commands/index.ts' import * as commands from './commands/index.ts' import { HelmDependencyManager, DependencyManager } from './core/dependency_managers/index.ts' import { - ChartManager, - ConfigManager, - PackageDownloader, - PlatformInstaller, - Helm, - logging, - KeyManager, Zippy, constants, ProfileManager + ChartManager, ConfigManager, PackageDownloader, PlatformInstaller, Helm, logging, + KeyManager, Zippy, constants, ProfileManager, AccountManager, LeaseManager } from './core/index.ts' import 'dotenv/config' import { K8 } from './core/k8.ts' -import { AccountManager } from './core/account_manager.ts' import { ListrLogger } from 'listr2' import { CustomProcessOutput } from './core/process_output.ts' import { type Opts } from './types/index.ts' @@ -57,6 +51,7 @@ export function main (argv: any) { const platformInstaller = new PlatformInstaller(logger, k8, configManager) const keyManager = new KeyManager(logger) const profileManager = new ProfileManager(logger, configManager) + const leaseManager = new LeaseManager(k8, logger, configManager) // set cluster and namespace in the global configManager from kubernetes context // so that we don't need to prompt the user @@ -75,7 +70,8 @@ export function main (argv: any) { depManager, keyManager, accountManager, - profileManager + profileManager, + leaseManager } const processArguments = (argv: any, yargs: any) => { diff --git a/src/types/index.ts b/src/types/index.ts index 26a174b63..37aad3c3a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -20,10 +20,9 @@ import type * as WebSocket from 'ws' import type crypto from 'crypto' import type { SoloLogger } from '../core/logging.ts' import type { - ChartManager, ConfigManager, Helm, K8, KeyManager, PackageDownloader, PlatformInstaller, ProfileManager + ChartManager, ConfigManager, Helm, K8, KeyManager, PackageDownloader, PlatformInstaller, + ProfileManager, DependencyManager, AccountManager, LeaseManager } from '../core/index.ts' -import type { DependencyManager } from '../core/dependency_managers/index.ts' -import type { AccountManager } from '../core/account_manager.ts' export interface NodeKeyObject { privateKey: crypto.webcrypto.CryptoKey @@ -79,4 +78,5 @@ export interface Opts { keyManager: KeyManager accountManager: AccountManager profileManager: ProfileManager + leaseManager: LeaseManager } diff --git a/test/e2e/e2e_node_util.ts b/test/e2e/e2e_node_util.ts index acc29ed3a..e1fad71b8 100644 --- a/test/e2e/e2e_node_util.ts +++ b/test/e2e/e2e_node_util.ts @@ -32,7 +32,7 @@ import { MINUTES, SECONDS } from '../../src/core/constants.ts' import type { NodeAlias } from '../../src/types/aliases.ts' import type { ListrTaskWrapper } from 'listr2' import { ConfigManager, type K8 } from '../../src/core/index.ts' -import { type NodeCommand } from '../../src/commands/node/index.js' +import { type NodeCommand } from '../../src/commands/node/index.ts' export function e2eNodeKeyRefreshTest (testName: string, mode: string, releaseTag = HEDERA_PLATFORM_VERSION_TAG) { const namespace = testName diff --git a/test/e2e/integration/core/lease_manager.test.ts b/test/e2e/integration/core/lease_manager.test.ts new file mode 100644 index 000000000..0c8331bf3 --- /dev/null +++ b/test/e2e/integration/core/lease_manager.test.ts @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { it, describe, after } from 'mocha' +import { expect } from 'chai' + +import { flags } from '../../../../src/commands/index.ts' +import { e2eTestSuite, getDefaultArgv, TEST_CLUSTER } from '../../../test_util.ts' +import * as version from '../../../../version.ts' +import { LEASE_AQUIRE_RETRY_TIMEOUT, MAX_LEASE_ACQUIRE_ATTEMPTS, MINUTES } from '../../../../src/core/constants.ts' +import { sleep } from '../../../../src/core/helpers.js' + +const namespace = 'lease-mngr-e2e' +const argv = getDefaultArgv() +argv[flags.namespace.name] = namespace +argv[flags.nodeAliasesUnparsed.name] = 'node1' +argv[flags.clusterName.name] = TEST_CLUSTER +argv[flags.soloChartVersion.name] = version.SOLO_CHART_VERSION +argv[flags.generateGossipKeys.name] = true +argv[flags.generateTlsKeys.name] = true +// set the env variable SOLO_CHARTS_DIR if developer wants to use local Solo charts +argv[flags.chartDirectory.name] = process.env.SOLO_CHARTS_DIR ?? undefined + +e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefined, undefined, false, (bootstrapResp) => { + describe('LeaseManager', async () => { + const k8 = bootstrapResp.opts.k8 + const leaseManager = bootstrapResp.opts.leaseManager + + after(async function () { + this.timeout(2 * MINUTES) + + await k8.deleteNamespace(namespace) + }) + + it('should be able to create lease and release it', async () => { + const lease = leaseManager.instantiateLease() + const title = 'Testing title' + // @ts-ignore to access private property + await lease.acquireTask({ title }, title) + + expect(typeof lease.release).to.equal('function') + await lease.release() + }) + + it('should not be able to create second lease in the same namespace', async () => { + // Create first lease + const initialLease = leaseManager.instantiateLease() + const title = 'Testing title' + // @ts-ignore to access private property + await initialLease.acquireTask({ title }, title) + + const blockedLease = leaseManager.instantiateLease() + + try { + // @ts-ignore to access private property + await blockedLease.acquireTask({ title }, title, MAX_LEASE_ACQUIRE_ATTEMPTS - 1) + + await sleep(LEASE_AQUIRE_RETRY_TIMEOUT * 2) + } catch (e: Error | any) { + expect(e.message).to.contain('Failed to acquire lease, max attempt reached') + } + + await initialLease.release() + }).timeout(3 * MINUTES) + }) +}) diff --git a/test/test_util.ts b/test/test_util.ts index 8771c5c85..97bdbf832 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -26,7 +26,6 @@ import { ClusterCommand } from '../src/commands/cluster.ts' import { InitCommand } from '../src/commands/init.ts' import { NetworkCommand } from '../src/commands/network.ts' import { NodeCommand } from '../src/commands/node/index.ts' -import { AccountManager } from '../src/core/account_manager.ts' import { DependencyManager, HelmDependencyManager @@ -38,13 +37,14 @@ import { constants, Helm, K8, - KeyManager, + KeyManager, LeaseManager, logging, PackageDownloader, PlatformInstaller, ProfileManager, Templates, - Zippy + Zippy, + AccountManager } from '../src/core/index.ts' import { flags } from '../src/commands/index.ts' import { @@ -104,6 +104,7 @@ interface TestOpts { accountManager: AccountManager cacheDir: string profileManager: ProfileManager + leaseManager: LeaseManager } interface BootstrapResponse { @@ -146,6 +147,8 @@ export function bootstrapTestVariables ( const accountManager = new AccountManager(testLogger, k8) const platformInstaller = new PlatformInstaller(testLogger, k8, configManager) const profileManager = new ProfileManager(testLogger, configManager) + const leaseManager = new LeaseManager(k8, testLogger, configManager) + const opts: TestOpts = { logger: testLogger, helm, @@ -158,7 +161,8 @@ export function bootstrapTestVariables ( keyManager, accountManager, cacheDir, - profileManager + profileManager, + leaseManager, } const initCmd = initCmdArg || new InitCommand(opts) @@ -256,7 +260,7 @@ export function e2eTestSuite ( flags.quiet.constName, flags.settingTxt.constName ]) - }).timeout(2 * MINUTES) + }).timeout(3 * MINUTES) if (startNodes) { it('should succeed with node setup command', async () => { diff --git a/test/unit/commands/init.test.ts b/test/unit/commands/init.test.ts index 9ead880a0..756806966 100644 --- a/test/unit/commands/init.test.ts +++ b/test/unit/commands/init.test.ts @@ -23,11 +23,7 @@ import { DependencyManager } from '../../../src/core/dependency_managers/index.ts' import { - ChartManager, - ConfigManager, constants, - Helm, - KeyManager, - logging, PackageDownloader, Zippy + ChartManager, ConfigManager, constants, Helm, KeyManager, LeaseManager, logging, PackageDownloader, Zippy } from '../../../src/core/index.ts' import { getK8Instance } from '../../test_util.ts' import { SECONDS } from '../../../src/core/constants.ts' @@ -49,10 +45,11 @@ describe('InitCommand', () => { const keyManager = new KeyManager(testLogger) const k8 = getK8Instance(configManager) + const leaseManager = new LeaseManager(k8, testLogger, configManager) // @ts-ignore const initCmd = new InitCommand({ - logger: testLogger, helm, k8, chartManager, configManager, depManager, keyManager + logger: testLogger, helm, k8, chartManager, configManager, depManager, keyManager, leaseManager }) describe('commands', () => {