@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
249 lines (218 loc) • 11 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {SoloError} from '../core/errors/solo-error.js';
import {ShellRunner} from '../core/shell-runner.js';
import {type LockManager} from '../core/lock/lock-manager.js';
import {type ChartManager} from '../core/chart-manager.js';
import {type ConfigManager} from '../core/config-manager.js';
import {type DependencyManager} from '../core/dependency-managers/index.js';
import {type K8Factory} from '../integration/kube/k8-factory.js';
import {type HelmClient} from '../integration/helm/helm-client.js';
import {type LocalConfigRuntimeState} from '../business/runtime-state/config/local/local-config-runtime-state.js';
import * as constants from '../core/constants.js';
import fs from 'node:fs';
import {
type ClusterReferenceName,
type ClusterReferences,
type ComponentId,
type Context,
NamespaceNameAsString,
Optional,
type SoloListrTaskWrapper,
} from '../types/index.js';
import {Flags as flags, Flags} from './flags.js';
import {inject} from 'tsyringe-neo';
import {patchInject} from '../core/dependency-injection/container-helper.js';
import {InjectTokens} from '../core/dependency-injection/inject-tokens.js';
import {type RemoteConfigRuntimeStateApi} from '../business/runtime-state/api/remote-config-runtime-state-api.js';
import {type TaskList} from '../core/task-list/task-list.js';
import {ListrContext, ListrRendererValue} from 'listr2';
import {type ComponentFactoryApi} from '../core/config/remote/api/component-factory-api.js';
import {type OneShotState} from '../core/one-shot-state.js';
import {NamespaceName} from '../types/namespace/namespace-name.js';
import {AnyListrContext} from '../types/aliases.js';
import {resolveNamespaceFromDeployment} from '../core/resolvers.js';
import {Templates} from '../core/templates.js';
import {BaseStateSchema} from '../data/schema/model/remote/state/base-state-schema.js';
import {ComponentTypes} from '../core/config/remote/enumerations/component-types.js';
import {NodeCommandTasks} from './node/tasks.js';
import {SoloConfig} from '../business/runtime-state/config/solo/solo-config.js';
import {type ConfigProvider} from '../data/configuration/api/config-provider.js';
export abstract class BaseCommand extends ShellRunner {
public readonly soloConfig: SoloConfig;
public constructor(
protected readonly helm?: HelmClient,
protected readonly k8Factory?: K8Factory,
protected readonly chartManager?: ChartManager,
public readonly configManager?: ConfigManager,
protected readonly depManager?: DependencyManager,
protected readonly leaseManager?: LockManager,
public readonly localConfig?: LocalConfigRuntimeState,
protected readonly remoteConfig?: RemoteConfigRuntimeStateApi,
protected readonly taskList?: TaskList<ListrContext, ListrRendererValue, ListrRendererValue>,
protected readonly componentFactory?: ComponentFactoryApi,
protected readonly oneShotState?: OneShotState,
protected readonly nodeCommandTasks?: NodeCommandTasks,
private readonly configProvider?: ConfigProvider,
) {
super();
this.helm = patchInject(helm, InjectTokens.Helm, this.constructor.name);
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
this.chartManager = patchInject(chartManager, InjectTokens.ChartManager, this.constructor.name);
this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
this.depManager = patchInject(depManager, InjectTokens.DependencyManager, this.constructor.name);
this.leaseManager = patchInject(leaseManager, InjectTokens.LockManager, this.constructor.name);
this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name);
this.remoteConfig = patchInject(remoteConfig, InjectTokens.RemoteConfigRuntimeState, this.constructor.name);
this.taskList = patchInject(taskList, InjectTokens.TaskList, this.constructor.name);
this.componentFactory = patchInject(componentFactory, InjectTokens.ComponentFactory, this.constructor.name);
this.oneShotState = patchInject(oneShotState, InjectTokens.OneShotState, this.constructor.name);
this.nodeCommandTasks = patchInject(nodeCommandTasks, InjectTokens.NodeCommandTasks, this.constructor.name);
this.configProvider = patchInject(configProvider, InjectTokens.ConfigProvider, this.constructor.name);
this.soloConfig = SoloConfig.getConfig(this.configProvider);
}
protected async loadRemoteConfigOrWarn(
argv: {_: string[]} & Record<string, unknown>,
validate: boolean = true,
skipConsensusNodesValidation: boolean = true,
): Promise<boolean> {
try {
await this.remoteConfig.loadAndValidate(argv, validate, skipConsensusNodesValidation);
return true;
} catch (error) {
this.logger.warn(
`Failed to load remote config; continuing destroy: ${error instanceof Error ? error.message : error}`,
);
return false;
}
}
public abstract close(): Promise<void>;
/**
* Setup home directories
* @param directories
*/
public setupHomeDirectory(directories: string[] = []): string[] {
if (!directories || directories?.length === 0) {
directories = [
constants.SOLO_HOME_DIR,
constants.SOLO_LOGS_DIR,
this.configManager.getFlag(Flags.cacheDir) || constants.SOLO_CACHE_DIR,
constants.SOLO_VALUES_DIR,
];
}
try {
for (const directoryPath of directories) {
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath, {recursive: true});
}
this.logger.debug(`OK: setup directory: ${directoryPath}`);
}
} catch (error) {
throw new SoloError(`failed to create directory: ${error.message}`, error);
}
return directories;
}
protected getClusterReference(): ClusterReferenceName {
const flagValue: ClusterReferenceName = this.configManager.getFlag(flags.clusterRef);
// If flag is provided, use it
if (flagValue) {
return flagValue;
}
// Try to auto-select if only one cluster exists in the deployment
try {
if (this.remoteConfig?.isLoaded()) {
const clusterReferences: ClusterReferences = this.remoteConfig.getClusterRefs();
if (clusterReferences.size === 1) {
// Auto-select the only available cluster
const clusterReference: ClusterReferenceName = [...clusterReferences.keys()][0];
this.logger.debug(`Auto-selected cluster reference: ${clusterReference} (only cluster in deployment)`);
return clusterReference;
} else if (clusterReferences.size > 1) {
// Multiple clusters exist - list them in error message
const clusterList: string = [...clusterReferences.keys()].join(', ');
throw new SoloError(`Multiple clusters found (${clusterList}). Please specify --cluster-ref to select one.`);
}
}
} catch (error) {
// If it's our SoloError about multiple clusters, re-throw it
if (error instanceof SoloError && error.message.includes('Multiple clusters found')) {
throw error;
}
// Otherwise, fall through to default behavior
this.logger.debug(`Could not auto-select cluster: ${error.message}`);
}
// Fall back to current cluster from kubeconfig
return this.k8Factory.default().clusters().readCurrent();
}
protected getClusterContext(clusterReference: ClusterReferenceName): Context {
return clusterReference
? this.localConfig.configuration.clusterRefs.get(clusterReference)?.toString()
: this.k8Factory.default().contexts().readCurrent();
}
protected getNamespace(task: SoloListrTaskWrapper<AnyListrContext>): Promise<NamespaceName> {
return resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
}
protected async throwIfNamespaceIsMissing(context: Context, namespace: NamespaceName): Promise<void> {
if (!(await this.k8Factory.getK8(context).namespaces().has(namespace))) {
throw new SoloError(`namespace ${namespace} does not exist`);
}
}
private inferMirrorNodeDataFromRemoteConfig(namespace: NamespaceName): {
mirrorNodeId: ComponentId;
mirrorNamespace: NamespaceNameAsString;
} {
let mirrorNodeId: ComponentId = this.configManager.getFlag(flags.mirrorNodeId);
let mirrorNamespace: NamespaceNameAsString = this.configManager.getFlag(flags.mirrorNamespace);
const mirrorNodeComponent: Optional<BaseStateSchema> =
this.remoteConfig.configuration.components.state.mirrorNodes[0];
if (!mirrorNodeId) {
mirrorNodeId = mirrorNodeComponent?.metadata.id ?? 1;
}
if (!mirrorNamespace) {
mirrorNamespace = mirrorNodeComponent?.metadata.namespace ?? namespace.name;
}
return {mirrorNodeId, mirrorNamespace};
}
protected async inferMirrorNodeData(
namespace: NamespaceName,
context: Context,
): Promise<{
mirrorNodeId: ComponentId;
mirrorNamespace: NamespaceNameAsString;
mirrorNodeReleaseName: string;
}> {
const {mirrorNodeId, mirrorNamespace} = this.inferMirrorNodeDataFromRemoteConfig(namespace);
const mirrorNodeReleaseName: string = await this.inferMirrorNodeReleaseName(mirrorNodeId, mirrorNamespace, context);
return {mirrorNodeId, mirrorNamespace, mirrorNodeReleaseName};
}
private async inferMirrorNodeReleaseName(
mirrorNodeId: ComponentId,
mirrorNodeNamespace: string,
context: Context,
): Promise<string> {
if (mirrorNodeId !== 1) {
return Templates.renderMirrorNodeName(mirrorNodeId);
}
// Try to get the component and use the precise cluster context
try {
const mirrorNodeComponent: BaseStateSchema = this.remoteConfig.configuration.components.getComponentById(
ComponentTypes.MirrorNode,
mirrorNodeId,
);
if (mirrorNodeComponent) {
context = this.getClusterContext(mirrorNodeComponent.metadata.cluster);
}
} catch {
// Guard
}
const isLegacyChartInstalled: boolean = await this.chartManager.isChartInstalled(
NamespaceName.of(mirrorNodeNamespace),
constants.MIRROR_NODE_RELEASE_NAME,
context,
);
return isLegacyChartInstalled ? constants.MIRROR_NODE_RELEASE_NAME : Templates.renderMirrorNodeName(mirrorNodeId);
}
protected async resolveNamespaceFromDeployment(task?: SoloListrTaskWrapper<AnyListrContext>): Promise<NamespaceName> {
return await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
}
}