@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
226 lines (199 loc) • 9.77 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {Flags as flags} from '../commands/flags.js';
import chalk from 'chalk';
import {type NamespaceName} from '../types/namespace/namespace-name.js';
import {type ConfigManager} from './config-manager.js';
import {type K8Factory} from '../integration/kube/k8-factory.js';
import {type SoloLogger} from './logging/solo-logger.js';
import {type AnyObject, ArgvStruct} from '../types/aliases.js';
import {type ClusterReferenceName} from './../types/index.js';
import {SilentBreak} from './errors/silent-break.js';
import {type HelpRenderer} from './help-renderer.js';
import {patchInject} from './dependency-injection/container-helper.js';
import {InjectTokens} from './dependency-injection/inject-tokens.js';
import {inject, injectable} from 'tsyringe-neo';
import {LocalConfigRuntimeState} from '../business/runtime-state/config/local/local-config-runtime-state.js';
import {type RemoteConfigRuntimeStateApi} from '../business/runtime-state/api/remote-config-runtime-state-api.js';
import {K8} from '../integration/kube/k8.js';
import {type TaskList} from './task-list/task-list.js';
import {Listr, ListrContext, ListrRendererValue} from 'listr2';
import {type InitCommand} from '../commands/init/init.js';
import {InitContext} from '../commands/init/init-context.js';
import {SoloError} from './errors/solo-error.js';
import {NpmClient} from '../integration/npm/npm-client.js';
export class Middlewares {
public constructor(
private readonly configManager: ConfigManager,
private readonly remoteConfig: RemoteConfigRuntimeStateApi,
private readonly k8Factory: K8Factory,
private readonly logger: SoloLogger,
private readonly localConfig: LocalConfigRuntimeState,
private readonly helpRenderer: HelpRenderer,
private readonly taskList: TaskList<ListrContext, ListrRendererValue, ListrRendererValue>,
private readonly initCommand: InitCommand,
private readonly npmClient: NpmClient,
) {
this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
this.remoteConfig = patchInject(remoteConfig, InjectTokens.RemoteConfigRuntimeState, this.constructor.name);
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name);
this.helpRenderer = patchInject(helpRenderer, InjectTokens.HelpRenderer, this.constructor.name);
this.taskList = patchInject(taskList, InjectTokens.TaskList, this.constructor.name);
this.initCommand = patchInject(initCommand, InjectTokens.InitCommand, this.constructor.name);
this.npmClient = patchInject(npmClient, InjectTokens.NpmClient, this.constructor.name);
}
public initSystemFiles(): (argv: ArgvStruct) => AnyObject {
return async (argv: ArgvStruct): Promise<AnyObject> => {
const tasks: Listr<InitContext, ListrRendererValue, ListrRendererValue> =
// @ts-expect-error - TS2445: Property taskList is protected and only accessible within class BaseCommand and its subclasses.
this.initCommand.taskList.newTaskList(this.initCommand.setupSystemFilesTasks(argv), {renderer: 'silent'});
if (tasks.isRoot()) {
try {
await tasks.run();
} catch (error: Error | any) {
throw new SoloError('Error initiating Solo system files', error);
}
}
return argv;
};
}
public printCustomHelp(rootCmd: any): (argv: ArgvStruct) => void {
/**
* @param argv - listr Argv
*/
return (argv: ArgvStruct): void => {
if (!argv['help']) {
return;
}
rootCmd.showHelp((output: string): void => {
this.helpRenderer.render(rootCmd, output);
});
throw new SilentBreak('printed help, exiting');
};
}
public setLoggerDevFlag(): (argv: ArgvStruct) => AnyObject {
const logger: SoloLogger = this.logger;
/**
* @param argv - listr Argv
*/
return (argv: ArgvStruct): AnyObject => {
if (argv.dev) {
logger.debug('Setting logger dev flag');
logger.setDevMode(argv.dev);
}
return argv;
};
}
public detectLocalSoloPackages(): (argv: ArgvStruct) => AnyObject {
const SOLO_PACKAGES_TO_UNLINK: string[] = ['@hashgraph/solo', '@hiero-ledger/solo'];
/**
* @param argv - listr Argv
*/
return async (argv: ArgvStruct): Promise<AnyObject> => {
try {
const listResult: string[] = await this.npmClient.listGlobal();
const foundLinkedPackages: string[] = [];
for (const item of listResult) {
// Check if any of the globally linked packages match the SOLO_PACKAGES_TO_UNLINK
// and unlink them if they point to a local directory (indicated by '->' in the npm list output)
const matchesSoloPackages: string[] = SOLO_PACKAGES_TO_UNLINK.filter(
(soloPackage: string): boolean => item.includes(soloPackage) && item.includes('->'),
);
for (const packageName of matchesSoloPackages) {
try {
const logMessage: string = `Warning: Found locally linked installation of ${packageName}.`;
this.logger.showUser(chalk.yellow(logMessage));
this.logger.info(logMessage);
foundLinkedPackages.push(packageName);
} catch (error: Error | unknown) {
this.logger.error(
new SoloError(
`Failed to parse npm list output line "${item}". Please check for any globally linked Solo packages and unlink them manually using "npm unlink -g <package-name>".`,
error,
),
);
}
}
}
} catch (error: Error | unknown) {
this.logger.warn(
new SoloError(
'Failed to detect globally linked Solo packages. Please check for any globally linked Solo packages and' +
' unlink them manually using "npm unlink -g <package-name>".',
error,
),
);
}
return argv;
};
}
/**
* Processes the Argv and display the command header
*
* @returns callback function to be executed from listr
*/
protected processArgumentsAndDisplayHeader(): (argv: ArgvStruct, yargs: any) => AnyObject {
const k8Factory: K8Factory = this.k8Factory;
const configManager: ConfigManager = this.configManager;
const logger: SoloLogger = this.logger;
/**
* @param argv - listr Argv
* @param yargs - listr Yargs
*/
return (argv: any, yargs: any): AnyObject => {
logger.debug('Processing arguments and displaying header');
let clusterName: string = 'N/A';
let contextName: string = 'N/A';
// reset config on `solo init` command
if (argv._[0] === 'init') {
configManager.reset();
}
// set cluster and namespace in the global configManager from kubernetes context
// so that we don't need to prompt the user
try {
const k8: K8 = k8Factory.default();
const contextNamespace: NamespaceName = k8.contexts().readCurrentNamespace();
const currentClusterName: string = k8.clusters().readCurrent();
contextName = k8.contexts().readCurrent();
clusterName = configManager.getFlag<ClusterReferenceName>(flags.clusterRef) || currentClusterName;
// Set namespace if not provided
if (contextNamespace?.name) {
configManager.setFlag(flags.namespace, contextNamespace);
}
} catch {
/* empty */
}
// apply precedence for flags
argv = configManager.applyPrecedence(argv, yargs.parsed.aliases);
// update config manager
configManager.update(argv);
// Build data to be displayed
const currentCommand: string = argv._.join(' ');
const commandArguments: string = flags.stringifyArgv(argv);
const commandData: string = (currentCommand + ' ' + commandArguments).trim();
// Check if output format is set (machine-readable modes: json, yaml, wide)
const outputFormat = configManager.getFlag<string>(flags.output) || '';
const isMachineReadable = ['json', 'yaml', 'wide'].includes(outputFormat);
if (this.taskList.parentTaskListMap.size === 0 && !isMachineReadable) {
// Display command header (skip in machine-readable output modes)
logger.showUser(
chalk.cyan('\n******************************* Solo *********************************************'),
);
logger.showUser(chalk.cyan('Version\t\t\t:'), chalk.yellow(configManager.getVersion()));
logger.showUser(chalk.cyan('Kubernetes Context\t:'), chalk.yellow(contextName));
logger.showUser(chalk.cyan('Kubernetes Cluster\t:'), chalk.yellow(clusterName));
logger.showUser(chalk.cyan('Current Command\t\t:'), chalk.yellow(commandData));
if (configManager.getFlag<NamespaceName>(flags.namespace)?.name) {
logger.showUser(chalk.cyan('Kubernetes Namespace\t:'), chalk.yellow(configManager.getFlag(flags.namespace)));
}
logger.showUser(
chalk.cyan('**********************************************************************************'),
);
}
return argv;
};
}
}