diff --git a/src/commands/account.mjs b/src/commands/account.mjs index bf18b4170..dcb56325c 100644 --- a/src/commands/account.mjs +++ b/src/commands/account.mjs @@ -147,7 +147,7 @@ export class AccountCommand extends BaseCommand { } }, { - title: 'create the new account', + title: 'Create the new account', task: async (ctx, task) => { self.accountInfo = await self.createNewAccount(ctx) const accountInfoCopy = { ...self.accountInfo } @@ -203,7 +203,7 @@ export class AccountCommand extends BaseCommand { } }, { - title: 'get the account info', + title: 'Get the account info', task: async (ctx, task) => { ctx.treasuryAccountInfo = await self.accountManager.getTreasuryAccountKeys(ctx.config.namespace) await self.loadNodeClient(ctx) @@ -211,7 +211,7 @@ export class AccountCommand extends BaseCommand { } }, { - title: 'update the account', + title: 'Update the account', task: async (ctx, task) => { if (!(await self.updateAccountInfo(ctx))) { throw new FullstackTestingError(`An error occurred updating account ${ctx.accountInfo.accountId}`) @@ -219,7 +219,7 @@ export class AccountCommand extends BaseCommand { } }, { - title: 'get the updated account info', + title: 'Get the updated account info', task: async (ctx, task) => { self.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, false) this.logger.showJSON('account info', self.accountInfo) @@ -270,7 +270,7 @@ export class AccountCommand extends BaseCommand { } }, { - title: 'get the account info', + title: 'Get the account info', task: async (ctx, task) => { ctx.treasuryAccountInfo = await self.accountManager.getTreasuryAccountKeys(ctx.config.namespace) await self.loadNodeClient(ctx) @@ -312,18 +312,8 @@ export class AccountCommand extends BaseCommand { flags.privateKey, flags.amount ), - handler: argv => { - accountCmd.logger.debug("==== Running 'account create' ===") - accountCmd.logger.debug(argv) - - accountCmd.create(argv).then(r => { - accountCmd.logger.debug("==== Finished running 'account create' ===") - if (!r) process.exit(1) - }).catch(err => { - accountCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => accountCmd.handleCommand( + argv, async (args) => await accountCmd.create(args)) }) .command({ command: 'update', @@ -334,18 +324,8 @@ export class AccountCommand extends BaseCommand { flags.privateKey, flags.amount ), - handler: argv => { - accountCmd.logger.debug("==== Running 'account update' ===") - accountCmd.logger.debug(argv) - - accountCmd.update(argv).then(r => { - accountCmd.logger.debug("==== Finished running 'account update' ===") - if (!r) process.exit(1) - }).catch(err => { - accountCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => accountCmd.handleCommand( + argv, async (args) => await accountCmd.update(args)) }) .command({ command: 'get', @@ -354,18 +334,8 @@ export class AccountCommand extends BaseCommand { flags.namespace, flags.accountId ), - handler: argv => { - accountCmd.logger.debug("==== Running 'account get' ===") - accountCmd.logger.debug(argv) - - accountCmd.get(argv).then(r => { - accountCmd.logger.debug("==== Finished running 'account get' ===") - if (!r) process.exit(1) - }).catch(err => { - accountCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => accountCmd.handleCommand( + argv, async (args) => await accountCmd.get(args)) }) .demandCommand(1, 'Select an account command') } diff --git a/src/commands/base.mjs b/src/commands/base.mjs index 78a113fa9..47c059a21 100644 --- a/src/commands/base.mjs +++ b/src/commands/base.mjs @@ -15,8 +15,10 @@ * */ 'use strict' -import { MissingArgumentError } from '../core/errors.mjs' +import { FullstackTestingError, MissingArgumentError } from '../core/errors.mjs' +import { ConfigManager } from '../core/index.mjs' import { ShellRunner } from '../core/shell_runner.mjs' +import * as helpers from '../core/helpers.mjs' export class BaseCommand extends ShellRunner { async prepareChartPath (chartDir, chartRepo, chartName) { @@ -48,4 +50,35 @@ export class BaseCommand extends ShellRunner { this.configManager = opts.configManager this.depManager = opts.depManager } + + /** + * Handle the execution of the command + * + * It ensures process file is locked before the handleFunc is called + * + * @param argv argv of the command + * @param handleFunc async function to be invoked + * @param errHandler error handler + * @return {Promise} + */ + async handleCommand (argv, handleFunc, errHandler = helpers.defaultErrorHandler) { + if (!argv) throw new MissingArgumentError('argv is required') + if (!handleFunc) throw new MissingArgumentError('handleFunc is required') + + let error = null + try { + this.logger.debug(`==== Start: '${argv._.join(' ')}' ===`, { config: this.configManager.config, argv }) + await ConfigManager.acquireProcessLock(this.logger) + await handleFunc(argv) + } catch (e) { + error = new FullstackTestingError(`Error occurred: ${e.message}`, e) + } finally { + await ConfigManager.releaseProcessLock(this.logger) + this.logger.debug(`==== End: '${argv._.join(' ')}' ===`, { config: this.configManager.config, argv }) + } + + if (error) { + return errHandler(error, this.logger) + } + } } diff --git a/src/commands/cluster.mjs b/src/commands/cluster.mjs index 821803d06..a2219199f 100644 --- a/src/commands/cluster.mjs +++ b/src/commands/cluster.mjs @@ -223,33 +223,14 @@ export class ClusterCommand extends BaseCommand { .command({ command: 'list', desc: 'List all available clusters', - handler: argv => { - clusterCmd.logger.debug("==== Running 'cluster list' ===", { argv }) - - clusterCmd.showClusterList().then(r => { - clusterCmd.logger.debug('==== Finished running `cluster list`====') - - if (!r) process.exit(1) - }).catch(err => { - clusterCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => clusterCmd.handleCommand( + argv, async (args) => await clusterCmd.showClusterList(args)) }) .command({ command: 'info', desc: 'Get cluster info', - handler: argv => { - clusterCmd.logger.debug("==== Running 'cluster info' ===", { argv }) - clusterCmd.getClusterInfo(argv).then(r => { - clusterCmd.logger.debug('==== Finished running `cluster info`====') - - if (!r) process.exit(1) - }).catch(err => { - clusterCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => clusterCmd.handleCommand( + argv, async (args) => await clusterCmd.getClusterInfo(args)) }) .command({ command: 'setup', @@ -264,18 +245,8 @@ export class ClusterCommand extends BaseCommand { flags.deployCertManagerCrds, flags.fstChartVersion ), - handler: argv => { - clusterCmd.logger.debug("==== Running 'cluster setup' ===", { argv }) - - clusterCmd.setup(argv).then(r => { - clusterCmd.logger.debug('==== Finished running `cluster setup`====') - - if (!r) process.exit(1) - }).catch(err => { - clusterCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => clusterCmd.handleCommand( + argv, async (args) => await clusterCmd.setup(args)) }) .command({ command: 'reset', @@ -284,18 +255,8 @@ export class ClusterCommand extends BaseCommand { flags.clusterName, flags.clusterSetupNamespace ), - handler: argv => { - clusterCmd.logger.debug("==== Running 'cluster reset' ===", { argv }) - - clusterCmd.reset(argv).then(r => { - clusterCmd.logger.debug('==== Finished running `cluster reset`====') - - if (!r) process.exit(1) - }).catch(err => { - clusterCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => clusterCmd.handleCommand( + argv, async (args) => await clusterCmd.reset(args)) }) .demandCommand(1, 'Select a cluster command') } diff --git a/src/commands/index.mjs b/src/commands/index.mjs index c6028e486..938c38d36 100644 --- a/src/commands/index.mjs +++ b/src/commands/index.mjs @@ -45,4 +45,13 @@ function Initialize (opts) { } // Expose components from the command module -export { Initialize, flags } +export { + Initialize, + InitCommand, + ClusterCommand, + NetworkCommand, + NodeCommand, + RelayCommand, + AccountCommand, + flags +} diff --git a/src/commands/init.mjs b/src/commands/init.mjs index aa139fc16..1a72c5018 100644 --- a/src/commands/init.mjs +++ b/src/commands/init.mjs @@ -163,14 +163,8 @@ export class InitCommand extends BaseCommand { flags.fstChartVersion ) }, - handler: (argv) => { - initCmd.init(argv).then(r => { - if (!r) process.exit(1) - }).catch(err => { - initCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => initCmd.handleCommand( + argv, async (args) => await initCmd.init(args)) } } } diff --git a/src/commands/network.mjs b/src/commands/network.mjs index b9066174a..0e7a939e5 100644 --- a/src/commands/network.mjs +++ b/src/commands/network.mjs @@ -367,19 +367,8 @@ export class NetworkCommand extends BaseCommand { flags.enablePrometheusSvcMonitor, flags.fstChartVersion ), - handler: argv => { - networkCmd.logger.debug("==== Running 'network deploy' ===") - networkCmd.logger.debug(argv) - - networkCmd.deploy(argv).then(r => { - networkCmd.logger.debug('==== Finished running `network deploy`====') - - if (!r) process.exit(1) - }).catch(err => { - networkCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => networkCmd.handleCommand( + argv, async (args) => await networkCmd.deploy(args)) }) .command({ command: 'destroy', @@ -389,19 +378,8 @@ export class NetworkCommand extends BaseCommand { flags.force, flags.deletePvcs ), - handler: argv => { - networkCmd.logger.debug("==== Running 'network destroy' ===") - networkCmd.logger.debug(argv) - - networkCmd.destroy(argv).then(r => { - networkCmd.logger.debug('==== Finished running `network destroy`====') - - if (!r) process.exit(1) - }).catch(err => { - networkCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => networkCmd.handleCommand( + argv, async (args) => await networkCmd.destroy(args)) }) .command({ command: 'refresh', @@ -418,19 +396,8 @@ export class NetworkCommand extends BaseCommand { flags.hederaExplorerTlsHostName, flags.enablePrometheusSvcMonitor ), - handler: argv => { - networkCmd.logger.debug("==== Running 'chart upgrade' ===") - networkCmd.logger.debug(argv) - - networkCmd.refresh(argv).then(r => { - networkCmd.logger.debug('==== Finished running `chart upgrade`====') - - if (!r) process.exit(1) - }).catch(err => { - networkCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => networkCmd.handleCommand( + argv, async (args) => await networkCmd.refresh(args)) }) .demandCommand(1, 'Select a chart command') } diff --git a/src/commands/node.mjs b/src/commands/node.mjs index b01965ad3..bd05318ea 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -67,8 +67,6 @@ export class NodeCommand extends BaseCommand { let attempt = 0 let isActive = false - await sleep(10000) // sleep in case this the user ran the start command again at a later time - // check log file is accessible let logFileAccessible = false while (attempt++ < maxAttempt) { @@ -77,7 +75,8 @@ export class NodeCommand extends BaseCommand { logFileAccessible = true break } - } catch (e) {} // ignore errors + } catch (e) { // ignore errors + } await sleep(1000) } @@ -91,7 +90,7 @@ export class NodeCommand extends BaseCommand { try { const output = await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['tail', '-10', logfilePath]) if (output.indexOf(`Terminating Netty = ${status}`) < 0 && // make sure we are not at the beginning of a restart - output.indexOf(`Now current platform status = ${status}`) > 0) { + output.indexOf(`Now current platform status = ${status}`) > 0) { this.logger.debug(`Node ${nodeId} is ${status} [ attempt: ${attempt}/${maxAttempt}]`) isActive = true break @@ -576,7 +575,7 @@ export class NodeCommand extends BaseCommand { // Retrieve the AddressBook as base64 return await this.accountManager.prepareAddressBookBase64(nodeClient) } catch (e) { - throw new FullstackTestingError('an error was encountered while trying to prepare the address book') + throw new FullstackTestingError(`an error was encountered while trying to prepare the address book: ${e.message}`, e) } finally { await this.accountManager.stopPortForwards() if (nodeClient) { @@ -674,12 +673,12 @@ export class NodeCommand extends BaseCommand { } if (!fs.existsSync(config.keysDir)) { - fs.mkdirSync(config.keysDir) + fs.mkdirSync(config.keysDir, { recursive: true }) } if (config.keyFormat === constants.KEY_FORMAT_PFX && config.generateGossipKeys) { throw new FullstackTestingError('Unable to generate PFX gossip keys.\n' + - `Please ensure you have pre-generated (*.pfx) key files in keys directory: ${config.keysDir}\n` + 'Use --key-format pem' ) } @@ -799,18 +798,8 @@ export class NodeCommand extends BaseCommand { flags.settingTxt, flags.log4j2Xml ), - handler: argv => { - nodeCmd.logger.debug("==== Running 'node setup' ===") - nodeCmd.logger.debug(argv) - - nodeCmd.setup(argv).then(r => { - nodeCmd.logger.debug('==== Finished running `node setup`====') - if (!r) process.exit(1) - }).catch(err => { - nodeCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => nodeCmd.handleCommand( + argv, async () => await nodeCmd.setup(argv)) }) .command({ command: 'start', @@ -820,18 +809,8 @@ export class NodeCommand extends BaseCommand { flags.nodeIDs, flags.updateAccountKeys ), - handler: argv => { - nodeCmd.logger.debug("==== Running 'node start' ===") - nodeCmd.logger.debug(argv) - - nodeCmd.start(argv).then(r => { - nodeCmd.logger.debug('==== Finished running `node start`====') - if (!r) process.exit(1) - }).catch(err => { - nodeCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => nodeCmd.handleCommand( + argv, async () => await nodeCmd.start(argv)) }) .command({ command: 'stop', @@ -840,18 +819,8 @@ export class NodeCommand extends BaseCommand { flags.namespace, flags.nodeIDs ), - handler: argv => { - nodeCmd.logger.debug("==== Running 'node stop' ===") - nodeCmd.logger.debug(argv) - - nodeCmd.stop(argv).then(r => { - nodeCmd.logger.debug('==== Finished running `node stop`====') - if (!r) process.exit(1) - }).catch(err => { - nodeCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => nodeCmd.handleCommand( + argv, async () => await nodeCmd.stop(argv)) }) .command({ command: 'keys', @@ -863,18 +832,8 @@ export class NodeCommand extends BaseCommand { flags.generateTlsKeys, flags.keyFormat ), - handler: argv => { - nodeCmd.logger.debug("==== Running 'node keys' ===") - nodeCmd.logger.debug(argv) - - nodeCmd.keys(argv).then(r => { - nodeCmd.logger.debug('==== Finished running `node keys`====') - if (!r) process.exit(1) - }).catch(err => { - nodeCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => nodeCmd.handleCommand( + argv, async () => await nodeCmd.keys(argv)) }) .demandCommand(1, 'Select a node command') } diff --git a/src/commands/relay.mjs b/src/commands/relay.mjs index e74097921..e36bda585 100644 --- a/src/commands/relay.mjs +++ b/src/commands/relay.mjs @@ -16,6 +16,7 @@ */ import { Listr } from 'listr2' import { FullstackTestingError, MissingArgumentError } from '../core/errors.mjs' +import * as helpers from '../core/helpers.mjs' import { BaseCommand } from './base.mjs' import * as flags from './flags.mjs' import * as paths from 'path' @@ -67,7 +68,7 @@ export class RelayCommand extends BaseCommand { return valuesArg } - prepareReleaseName (nodeIDs = []) { + prepareReleaseName (nodeIDs) { if (!nodeIDs) { throw new MissingArgumentError('Node IDs must be specified') } @@ -87,35 +88,30 @@ export class RelayCommand extends BaseCommand { title: 'Initialize', task: async (ctx, task) => { self.configManager.update(argv) + await prompts.execute(task, self.configManager, [ + flags.namespace, + flags.chartDirectory, + flags.valuesFile, + flags.nodeIDs, + flags.chainId, + flags.relayReleaseTag, + flags.replicaCount, + flags.operatorId, + flags.operatorKey + ]) - // extract config values - const valuesFile = self.configManager.getFlag(flags.valuesFile) - const nodeIds = self.configManager.getFlag(flags.nodeIDs) - const chainId = self.configManager.getFlag(flags.chainId) - const relayRelease = self.configManager.getFlag(flags.relayReleaseTag) - const replicaCount = self.configManager.getFlag(flags.replicaCount) - const operatorId = self.configManager.getFlag(flags.operatorId) - const operatorKey = self.configManager.getFlag(flags.operatorKey) - - const namespace = self.configManager.getFlag(flags.namespace) - const chartDir = self.configManager.getFlag(flags.chartDirectory) - - // prompt if inputs are empty and set it in the context - const namespaces = await self.k8.getNamespaces() ctx.config = { - chartDir: await prompts.promptChartDir(task, chartDir), - namespace: await prompts.promptSelectNamespaceArg(task, namespace, namespaces), - valuesFile: await prompts.promptValuesFile(task, valuesFile), - nodeIds: await prompts.promptNodeIds(task, nodeIds), - chainId: await prompts.promptChainId(task, chainId), - relayRelease: await prompts.promptRelayReleaseTag(task, relayRelease), - replicaCount: await prompts.promptReplicaCount(task, replicaCount), - operatorId: await prompts.promptOperatorId(task, operatorId), - operatorKey: await prompts.promptOperatorId(task, operatorKey) + chartDir: self.configManager.getFlag(flags.chartDirectory), + namespace: self.configManager.getFlag(flags.namespace), + valuesFile: self.configManager.getFlag(flags.valuesFile), + nodeIds: helpers.parseNodeIDs(self.configManager.getFlag(flags.nodeIDs)), + chainId: self.configManager.getFlag(flags.chainId), + relayRelease: self.configManager.getFlag(flags.relayReleaseTag), + replicaCount: self.configManager.getFlag(flags.replicaCount), + operatorId: self.configManager.getFlag(flags.operatorId), + operatorKey: self.configManager.getFlag(flags.operatorKey) } - self.logger.debug('Finished prompts', { ctx }) - ctx.releaseName = this.prepareReleaseName(ctx.config.nodeIds) ctx.isChartInstalled = await this.chartManager.isChartInstalled(ctx.config.namespace, ctx.releaseName) @@ -125,7 +121,7 @@ export class RelayCommand extends BaseCommand { { title: 'Prepare chart values', task: async (ctx, _) => { - ctx.chartPath = await this.prepareChartPath(ctx.config.chartDir, constants.JSON_RPC_RELAY_CHART, constants.CHART_JSON_RPC_RELAY_NAME) + ctx.chartPath = await this.prepareChartPath(ctx.config.chartDir, constants.JSON_RPC_RELAY_CHART, constants.JSON_RPC_RELAY_CHART) ctx.valuesArg = this.prepareValuesArg( ctx.config.valuesFile, ctx.config.nodeIds, @@ -178,19 +174,19 @@ export class RelayCommand extends BaseCommand { task: async (ctx, task) => { self.configManager.update(argv) - // extract config values - const nodeIds = self.configManager.getFlag(flags.nodeIDs) - const namespace = self.configManager.getFlag(flags.namespace) + await prompts.execute(task, self.configManager, [ + flags.namespace, + flags.nodeIDs + ]) - // prompt if inputs are empty and set it in the context - const namespaces = await self.k8.getNamespaces() ctx.config = { - namespace: await prompts.promptSelectNamespaceArg(task, namespace, namespaces), - nodeIds: await prompts.promptNodeIds(task, nodeIds) + namespace: self.configManager.getFlag(flags.namespace), + nodeIds: helpers.parseNodeIDs(self.configManager.getFlag(flags.nodeIDs)) } - ctx.config.releaseName = this.prepareReleaseName(ctx.config.nodeIds) self.logger.debug('Finished ctx initialization', { ctx }) + + ctx.config.releaseName = this.prepareReleaseName(ctx.config.nodeIds) } }, { @@ -240,18 +236,8 @@ export class RelayCommand extends BaseCommand { flags.operatorKey ) }, - handler: argv => { - relayCmd.logger.debug("==== Running 'relay install' ===", { argv }) - - relayCmd.install(argv).then(r => { - relayCmd.logger.debug('==== Finished running `relay install`====') - - if (!r) process.exit(1) - }).catch(err => { - relayCmd.logger.showUserError(err) - process.exit(1) - }) - } + handler: argv => relayCmd.handleCommand( + argv, async (args) => await relayCmd.install(args)) }) .command({ command: 'uninstall', @@ -260,16 +246,8 @@ export class RelayCommand extends BaseCommand { flags.namespace, flags.nodeIDs ), - handler: argv => { - relayCmd.logger.debug("==== Running 'relay uninstall' ===", { argv }) - relayCmd.logger.debug(argv) - - relayCmd.uninstall(argv).then(r => { - relayCmd.logger.debug('==== Finished running `relay uninstall`====') - - if (!r) process.exit(1) - }) - } + handler: argv => relayCmd.handleCommand( + argv, async (args) => await relayCmd.uninstall(args)) }) .demandCommand(1, 'Select a relay command') } diff --git a/src/core/config_manager.mjs b/src/core/config_manager.mjs index ae1f26436..e6c17e950 100644 --- a/src/core/config_manager.mjs +++ b/src/core/config_manager.mjs @@ -15,11 +15,11 @@ * */ import fs from 'fs' +import path from 'path' import { FullstackTestingError, MissingArgumentError } from './errors.mjs' import { constants } from './index.mjs' import { Logger } from './logging.mjs' import * as flags from '../commands/flags.mjs' -import * as paths from 'path' import * as helpers from './helpers.mjs' /** @@ -116,8 +116,9 @@ export class ConfigManager { case 'string': if (flag.name === flags.chartDirectory.name || flag.name === flags.cacheDir.name) { this.logger.debug(`Resolving directory path for '${flag.name}': ${val}`) - val = paths.resolve(val) + val = path.resolve(path.normalize(val)) } + this.logger.debug(`Setting flag '${flag.name}' of type '${flag.definition.type}': ${val}`) this.config.flags[flag.name] = `${val}` // force convert to string break @@ -156,6 +157,11 @@ export class ConfigManager { if (persist) { this.persist() } + + this.logger.debug('Updated cached config', { + argv, + config: this.config + }) } } @@ -225,4 +231,61 @@ export class ConfigManager { getUpdatedAt () { return this.config.updatedAt } + + /** + * Acquire process lock + * + * If no lock file exists, it will create a lock file and write the pid in the file + * @param logger + * @return {Promise} + */ + static async acquireProcessLock (logger) { + const pid = process.pid.toString() + const pidFile = path.normalize(constants.SOLO_PID_FILE) + if (!fs.existsSync(pidFile)) { + fs.writeFileSync(pidFile, pid) + logger.debug(`Acquired process lock '${pid}'`, { + pidFile, + pid + }) + return true + } + + // pid lock exists + const existingPid = fs.readFileSync(pidFile).toString() + throw new FullstackTestingError(`Process lock exists: ${constants.SOLO_PID_FILE}` + + `\nEnsure process '${existingPid}' is not running [ ps -p ${existingPid} ]`) + } + + /** + * Release process lock + * + * If current pid matches with the contents of the pid lock file, it will delete the lock file. + * Otherwise, it will log a warning + * + * @param logger + * @return {Promise} + */ + static async releaseProcessLock (logger) { + const pidFile = path.normalize(constants.SOLO_PID_FILE) + if (fs.existsSync(pidFile)) { + const existingPid = fs.readFileSync(pidFile).toString() + const pid = process.pid.toString() + + if (existingPid === process.pid.toString()) { + logger.debug(`Releasing process lock '${pid}'`, { + pidFile, + pid + }) + + fs.rmSync(pidFile) + } else { + logger.warn(`Unable to release process lock '${pid}'.\nEnsure process '${existingPid}' is not running [ps -p ${existingPid}]`, { + pidFile, + pid, + existingPid + }) + } + } + } } diff --git a/src/core/constants.mjs b/src/core/constants.mjs index 00de7f220..ddfb3b4e3 100644 --- a/src/core/constants.mjs +++ b/src/core/constants.mjs @@ -16,22 +16,26 @@ */ import { AccountId } from '@hashgraph/sdk' import { color, PRESET_TIMER } from 'listr2' -import { dirname, normalize } from 'path' +import os from 'os' +import * as path from 'path' import { fileURLToPath } from 'url' import chalk from 'chalk' // -------------------- solo related constants --------------------------------------------------------------------- -export const CUR_FILE_DIR = dirname(fileURLToPath(import.meta.url)) -export const USER = `${process.env.USER}` -export const USER_SANITIZED = USER.replace(/[\W_]+/g, '-') -export const SOLO_HOME_DIR = process.env.SOLO_HOME || `${process.env.HOME}/.solo` -export const SOLO_LOGS_DIR = `${SOLO_HOME_DIR}/logs` -export const SOLO_CACHE_DIR = `${SOLO_HOME_DIR}/cache` +export const SOLO_HOME_DIR = process.env.SOLO_HOME_DIR || path.resolve(path.join(process.env.HOME, '.solo')) +export const CUR_FILE_DIR = path.dirname(fileURLToPath(import.meta.url)) +export const SOLO_INSTALLATION_DIR = path.resolve(path.join(CUR_FILE_DIR, '/../..')) +// resources directory of solo where various templates exists +export const RESOURCES_DIR = path.resolve(path.join(SOLO_INSTALLATION_DIR, 'resources')) +export const SOLO_TMP_DIR = process.env.SOLO_TMP_DIR || os.tmpdir() + +export const SOLO_LOGS_DIR = path.join(SOLO_HOME_DIR, 'logs') +export const SOLO_PID_FILE = path.join(SOLO_HOME_DIR, 'solo.pid') +export const SOLO_CACHE_DIR = path.join(SOLO_HOME_DIR, 'cache') +export const SOLO_CONFIG_FILE = path.join(SOLO_HOME_DIR, 'solo.config') + export const DEFAULT_NAMESPACE = 'default' export const HELM = 'helm' -export const CWD = process.cwd() -export const SOLO_CONFIG_FILE = `${SOLO_HOME_DIR}/solo.config` -export const RESOURCES_DIR = normalize(CUR_FILE_DIR + '/../../resources') export const ROOT_CONTAINER = 'root-container' diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 3921aaac3..5c19d7ba6 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -15,9 +15,11 @@ * */ import fs from 'fs' +import path from 'path' import { FullstackTestingError } from './errors.mjs' import * as paths from 'path' import { fileURLToPath } from 'url' +import { constants } from './index.mjs' // cache current directory const CUR_FILE_DIR = paths.dirname(fileURLToPath(import.meta.url)) @@ -132,3 +134,17 @@ export function getRootImageRepository (releaseTag) { return 'hashgraph/full-stack-testing/ubi8-init-java21' } + +export function getTmpDir () { + return fs.mkdtempSync(path.join(constants.SOLO_TMP_DIR, 'solo-')) +} + +/** + * This is a default command error handler used by handleCommand + * @param err error + * @param logger logger + */ +export function defaultErrorHandler (err, logger) { + // TODO add user friendly message for the error + logger.showUserError(err) +} diff --git a/src/core/logging.mjs b/src/core/logging.mjs index f0bbeb2cf..7e1d3cdbc 100644 --- a/src/core/logging.mjs +++ b/src/core/logging.mjs @@ -124,7 +124,8 @@ export const Logger = class { } } - console.log(chalk.red('*********************************** ERROR *****************************************')) + console.log(chalk.red('*********************************** ERROR ****************************************')) + console.log(chalk.yellow(`Error Reference: ${this.traceId}`)) if (this.devMode) { let prefix = '' let indent = '' diff --git a/test/test_util.js b/test/test_util.js index a75100759..d6130d20b 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -15,9 +15,8 @@ * */ import fs from 'fs' -import os from 'os' import path from 'path' -import { logging } from '../src/core/index.mjs' +import { logging, constants } from '../src/core/index.mjs' export const testLogger = logging.NewLogger('debug') @@ -32,5 +31,5 @@ export function getTestCacheDir (appendDir) { } export function getTmpDir () { - return fs.mkdtempSync(path.join(os.tmpdir(), 'solo-')) + return fs.mkdtempSync(path.join(constants.SOLO_TMP_DIR, 'solo-test-')) } diff --git a/test/unit/commands/base.test.mjs b/test/unit/commands/base.test.mjs index 6726f9f26..f2ae37959 100644 --- a/test/unit/commands/base.test.mjs +++ b/test/unit/commands/base.test.mjs @@ -15,7 +15,9 @@ * */ import { expect, it, describe } from '@jest/globals' +import fs from 'fs' import { + constants, DependencyManager, ChartManager, ConfigManager, @@ -51,4 +53,38 @@ describe('BaseCommand', () => { await expect(baseCmd.run('date')).resolves.not.toBeNull() }) }) + + describe('handle command', () => { + it('should succeed in running a valid command handler', async () => { + expect.assertions(2) + expect(fs.existsSync(constants.SOLO_PID_FILE)).toBeFalsy() + + const argv = {} + argv._ = ['test'] + + let error = null + await baseCmd.handleCommand(argv, + async () => { + }, + (err, logger) => { + error = err + } + ) + expect(error).toBeNull() + }) + + it('should throw error if it fails to do process lock', async () => { + expect.assertions(2) + expect(fs.existsSync(constants.SOLO_PID_FILE)).toBeFalsy() + await ConfigManager.acquireProcessLock(testLogger) + + const argv = {} + argv._ = ['test'] + await baseCmd.handleCommand(argv, () => {}, (err, logger) => { + expect(err.message.includes('Process lock exists')).toBeTruthy() + }) + + await ConfigManager.releaseProcessLock(testLogger) + }) + }) }) diff --git a/test/unit/commands/init.test.mjs b/test/unit/commands/init.test.mjs index 311de9e58..0b91e7fc9 100644 --- a/test/unit/commands/init.test.mjs +++ b/test/unit/commands/init.test.mjs @@ -14,7 +14,7 @@ * limitations under the License. * */ -import { InitCommand } from '../../../src/commands/init.mjs' +import { InitCommand } from '../../../src/commands/index.mjs' import { expect, describe, it } from '@jest/globals' import { ChartManager, diff --git a/test/unit/core/config_manager.test.mjs b/test/unit/core/config_manager.test.mjs index 0c3a8dc8e..72d16e727 100644 --- a/test/unit/core/config_manager.test.mjs +++ b/test/unit/core/config_manager.test.mjs @@ -17,7 +17,7 @@ import { afterAll, describe, expect, it } from '@jest/globals' import os from 'os' import path from 'path' -import { ConfigManager } from '../../../src/core/index.mjs' +import { ConfigManager, constants } from '../../../src/core/index.mjs' import * as flags from '../../../src/commands/flags.mjs' import fs from 'fs' import { testLogger } from '../../test_util.js' @@ -43,6 +43,7 @@ describe('ConfigManager', () => { expect(cachedConfig.updatedAt).toStrictEqual(cm.getUpdatedAt()) expect(cachedConfig.version).toStrictEqual(cm.getVersion()) expect(cachedConfig.flags).toStrictEqual(cm.config.flags) + expect(cm.getUpdatedAt()).toStrictEqual(cachedConfig.updatedAt) fs.rmSync(tmpDir, { recursive: true }) }) @@ -51,6 +52,50 @@ describe('ConfigManager', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-')) const tmpFile = path.join(tmpDir, 'test.json') + it('should not persist force flag', () => { + const cm = new ConfigManager(testLogger, tmpFile) + const argv = {} + argv[flags.force.name] = true + cm.update(argv) + expect(cm.getFlag(flags.force)).toStrictEqual(undefined) + cm.persist() + expect(cm.getFlag(flags.force)).toStrictEqual(undefined) + cm.load() + expect(cm.getFlag(flags.force)).toStrictEqual(undefined) + }) + + it('should update cache-dir flag', () => { + const cm = new ConfigManager(testLogger, tmpFile) + const argv = {} + argv[flags.cacheDir.name] = os.tmpdir() + + try { + cm.update(argv) + } catch (e) { + } + + cm.reset() + argv[flags.cacheDir.name] = `${constants.SOLO_HOME_DIR}/new-cache` + cm.update(argv) + expect(cm.getFlag(flags.cacheDir)).toStrictEqual(`${constants.SOLO_HOME_DIR}/new-cache`) + }) + + it('should update chart-directory flag', () => { + const cm = new ConfigManager(testLogger, tmpFile) + const argv = {} + argv[flags.chartDirectory.name] = os.tmpdir() + + try { + cm.update(argv) + } catch (e) { + } + + cm.reset() + argv[flags.chartDirectory.name] = `${constants.SOLO_HOME_DIR}/charts` + cm.update(argv) + expect(cm.getFlag(flags.chartDirectory)).toStrictEqual(`${constants.SOLO_HOME_DIR}/charts`) + }) + it('should update string flag value', () => { const cm = new ConfigManager(testLogger, tmpFile) const argv = {} @@ -266,4 +311,23 @@ describe('ConfigManager', () => { expect(cm.getFlag(flags.namespace)).toBe(argv[flags.namespace.name]) }) }) + + describe('process lock', () => { + it('should be able to acquire process lock', async () => { + expect.assertions(6) + expect(fs.existsSync(constants.SOLO_PID_FILE)).toBeFalsy() + expect(await ConfigManager.acquireProcessLock(testLogger)).toBeTruthy() + expect(fs.existsSync(constants.SOLO_PID_FILE)).toBeTruthy() + expect(fs.readFileSync(constants.SOLO_PID_FILE).toString()).toStrictEqual(process.pid.toString()) + + // re-attempt should fail + try { + await ConfigManager.acquireProcessLock(testLogger) + } catch (e) { + expect(e.message.startsWith('Process lock exists')).toBeTruthy() + } + await ConfigManager.releaseProcessLock(testLogger) + expect(fs.existsSync(constants.SOLO_PID_FILE)).toBeFalsy() + }) + }) }) diff --git a/test/unit/core/helpers.test.mjs b/test/unit/core/helpers.test.mjs index 46c558a83..02e6d1979 100644 --- a/test/unit/core/helpers.test.mjs +++ b/test/unit/core/helpers.test.mjs @@ -14,9 +14,11 @@ * limitations under the License. * */ -import { describe, expect, it } from '@jest/globals' +import { describe, expect, it, jest } from '@jest/globals' import { FullstackTestingError } from '../../../src/core/errors.mjs' +import { Logger } from '../../../src/core/logging.mjs' import * as helpers from '../../../src/core/helpers.mjs' +import { testLogger } from '../../test_util.js' describe('Helpers', () => { it.each([ @@ -70,4 +72,14 @@ describe('Helpers', () => { expect(helpers.compareVersion('v3.12.3', 'v3.14.0')).toBe(1) }) }) + + describe('default error handler', () => { + it('should exit with error code on error', () => { + expect.assertions(1) + const spy = jest.spyOn(Logger.prototype, 'showUserError').mockImplementation() + const err = new FullstackTestingError('test') + helpers.defaultErrorHandler(err, testLogger) + expect(spy).toHaveBeenCalledWith(err) + }) + }) })