UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

249 lines (218 loc) 11 kB
// 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( @inject(InjectTokens.Helm) protected readonly helm?: HelmClient, @inject(InjectTokens.K8Factory) protected readonly k8Factory?: K8Factory, @inject(InjectTokens.ChartManager) protected readonly chartManager?: ChartManager, @inject(InjectTokens.ConfigManager) public readonly configManager?: ConfigManager, @inject(InjectTokens.DependencyManager) protected readonly depManager?: DependencyManager, @inject(InjectTokens.LockManager) protected readonly leaseManager?: LockManager, @inject(InjectTokens.LocalConfigRuntimeState) public readonly localConfig?: LocalConfigRuntimeState, @inject(InjectTokens.RemoteConfigRuntimeState) protected readonly remoteConfig?: RemoteConfigRuntimeStateApi, @inject(InjectTokens.TaskList) protected readonly taskList?: TaskList<ListrContext, ListrRendererValue, ListrRendererValue>, @inject(InjectTokens.ComponentFactory) protected readonly componentFactory?: ComponentFactoryApi, @inject(InjectTokens.OneShotState) protected readonly oneShotState?: OneShotState, @inject(InjectTokens.NodeCommandTasks) protected readonly nodeCommandTasks?: NodeCommandTasks, @inject(InjectTokens.ConfigProvider) 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); } }