UNPKG

@hashgraph/solo

Version:

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

435 lines (382 loc) 14.5 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import paths from 'path'; import {MissingArgumentError, SoloError} from '../core/errors.js'; import {ShellRunner} from '../core/shell_runner.js'; import {type LeaseManager} from '../core/lease/lease_manager.js'; import {type LocalConfig} from '../core/config/local_config.js'; import {type RemoteConfigManager} from '../core/config/remote/remote_config_manager.js'; import {type Helm} from '../core/helm.js'; import {type K8Factory} from '../core/kube/k8_factory.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 CommandFlag} from '../types/flag_types.js'; import {type Lease} from '../core/lease/lease.js'; import {Listr} from 'listr2'; import path from 'path'; import * as constants from '../core/constants.js'; import fs from 'fs'; import {Task} from '../core/task.js'; import {ConsensusNode} from '../core/model/consensus_node.js'; import {type ClusterRef, type ClusterRefs} from '../core/config/remote/types.js'; import {Flags} from './flags.js'; import {type Cluster} from '../core/config/remote/cluster.js'; import {Templates} from '../core/templates.js'; import {type SoloLogger} from '../core/logging.js'; import {type PackageDownloader} from '../core/package_downloader.js'; import {type PlatformInstaller} from '../core/platform_installer.js'; import {type KeyManager} from '../core/key_manager.js'; import {type AccountManager} from '../core/account_manager.js'; import {type ProfileManager} from '../core/profile_manager.js'; import {type CertificateManager} from '../core/certificate_manager.js'; import {type NodeAlias} from '../types/aliases.js'; export interface CommandHandlers { parent: BaseCommand; } export interface Opts { logger: SoloLogger; helm: Helm; k8Factory: K8Factory; downloader: PackageDownloader; platformInstaller: PlatformInstaller; chartManager: ChartManager; configManager: ConfigManager; depManager: DependencyManager; keyManager: KeyManager; accountManager: AccountManager; profileManager: ProfileManager; leaseManager: LeaseManager; certificateManager: CertificateManager; localConfig: LocalConfig; remoteConfigManager: RemoteConfigManager; parent?: BaseCommand; } export abstract class BaseCommand extends ShellRunner { protected readonly helm: Helm; protected readonly k8Factory: K8Factory; protected readonly chartManager: ChartManager; protected readonly configManager: ConfigManager; protected readonly depManager: DependencyManager; protected readonly leaseManager: LeaseManager; protected readonly _configMaps = new Map<string, any>(); public readonly localConfig: LocalConfig; protected readonly remoteConfigManager: RemoteConfigManager; constructor(opts: Opts) { if (!opts || !opts.helm) throw new Error('An instance of core/Helm is required'); if (!opts || !opts.k8Factory) throw new Error('An instance of core/K8Factory is required'); if (!opts || !opts.chartManager) throw new Error('An instance of core/ChartManager is required'); if (!opts || !opts.configManager) throw new Error('An instance of core/ConfigManager is required'); if (!opts || !opts.depManager) throw new Error('An instance of core/DependencyManager is required'); if (!opts || !opts.localConfig) throw new Error('An instance of core/LocalConfig is required'); if (!opts || !opts.remoteConfigManager) throw new Error('An instance of core/config/RemoteConfigManager is required'); super(); this.helm = opts.helm; this.k8Factory = opts.k8Factory; this.chartManager = opts.chartManager; this.configManager = opts.configManager; this.depManager = opts.depManager; this.leaseManager = opts.leaseManager; this.localConfig = opts.localConfig; this.remoteConfigManager = opts.remoteConfigManager; } public async prepareChartPath(chartDir: string, chartRepo: string, chartReleaseName: string) { if (!chartRepo) throw new MissingArgumentError('chart repo name is required'); if (!chartReleaseName) throw new MissingArgumentError('chart release name is required'); if (chartDir) { const chartPath = path.join(chartDir, chartReleaseName); await this.helm.dependency('update', chartPath); return chartPath; } return `${chartRepo}/${chartReleaseName}`; } // FIXME @Deprecated. Use prepareValuesFilesMap instead to support multi-cluster public prepareValuesFiles(valuesFile: string) { let valuesArg = ''; if (valuesFile) { const valuesFiles = valuesFile.split(','); valuesFiles.forEach(vf => { const vfp = paths.resolve(vf); valuesArg += ` --values ${vfp}`; }); } return valuesArg; } /** * Prepare the values files map for each cluster * * <p> Order of precedence: * <ol> * <li> Chart's default values file (if chartDirectory is set) </li> * <li> Profile values file </li> * <li> User's values file </li> * </ol> * @param clusterRefs - the map of cluster references * @param valuesFileInput - the values file input string * @param chartDirectory - the chart directory * @param profileValuesFile - the profile values file */ static prepareValuesFilesMap( clusterRefs: ClusterRefs, chartDirectory?: string, profileValuesFile?: string, valuesFileInput?: string, ): Record<ClusterRef, string> { // initialize the map with an empty array for each cluster-ref const valuesFiles: Record<ClusterRef, string> = { [Flags.KEY_COMMON]: '', }; Object.keys(clusterRefs).forEach(clusterRef => { valuesFiles[clusterRef] = ''; }); // add the chart's default values file for each cluster-ref if chartDirectory is set // this should be the first in the list of values files as it will be overridden by user's input if (chartDirectory) { const chartValuesFile = path.join(chartDirectory, 'solo-deployment', 'values.yaml'); for (const clusterRef in valuesFiles) { valuesFiles[clusterRef] += ` --values ${chartValuesFile}`; } } if (profileValuesFile) { const parsed = Flags.parseValuesFilesInput(profileValuesFile); Object.entries(parsed).forEach(([clusterRef, files]) => { let vf = ''; files.forEach(file => { vf += ` --values ${file}`; }); if (clusterRef === Flags.KEY_COMMON) { Object.entries(valuesFiles).forEach(([cf]) => { valuesFiles[cf] += vf; }); } else { valuesFiles[clusterRef] += vf; } }); } if (valuesFileInput) { const parsed = Flags.parseValuesFilesInput(valuesFileInput); Object.entries(parsed).forEach(([clusterRef, files]) => { let vf = ''; files.forEach(file => { vf += ` --values ${file}`; }); if (clusterRef === Flags.KEY_COMMON) { Object.entries(valuesFiles).forEach(([clusterRef]) => { valuesFiles[clusterRef] += vf; }); } else { valuesFiles[clusterRef] += vf; } }); } if (Object.keys(valuesFiles).length > 1) { // delete the common key if there is another cluster to use delete valuesFiles[Flags.KEY_COMMON]; } return valuesFiles; } public getConfigManager(): ConfigManager { return this.configManager; } public getChartManager(): ChartManager { return this.chartManager; } /** * Dynamically builds a class with properties from the provided list of flags * and extra properties, will keep track of which properties are used. Call * getUnusedConfigs() to get an array of unused properties. */ public getConfig(configName: string, flags: CommandFlag[], extraProperties: string[] = []): object { const configManager = this.configManager; // build the dynamic class that will keep track of which properties are used const NewConfigClass = class { private usedConfigs: Map<string, number>; constructor() { // the map to keep track of which properties are used this.usedConfigs = new Map(); // add the flags as properties to this class flags?.forEach(flag => { // @ts-ignore this[`_${flag.constName}`] = configManager.getFlag(flag); Object.defineProperty(this, flag.constName, { get() { this.usedConfigs.set(flag.constName, this.usedConfigs.get(flag.constName) + 1 || 1); return this[`_${flag.constName}`]; }, }); }); // add the extra properties as properties to this class extraProperties?.forEach(name => { // @ts-ignore this[`_${name}`] = ''; Object.defineProperty(this, name, { get() { this.usedConfigs.set(name, this.usedConfigs.get(name) + 1 || 1); return this[`_${name}`]; }, set(value) { this[`_${name}`] = value; }, }); }); } /** Get the list of unused configurations that were not accessed */ getUnusedConfigs() { const unusedConfigs: string[] = []; // add the flag constName to the unusedConfigs array if it was not accessed flags?.forEach(flag => { if (!this.usedConfigs.has(flag.constName)) { unusedConfigs.push(flag.constName); } }); // add the extra properties to the unusedConfigs array if it was not accessed extraProperties?.forEach(item => { if (!this.usedConfigs.has(item)) { unusedConfigs.push(item); } }); return unusedConfigs; } }; const newConfigInstance = new NewConfigClass(); // add the new instance to the configMaps so that it can be used to get the // unused configurations using the configName from the BaseCommand this._configMaps.set(configName, newConfigInstance); return newConfigInstance; } public getLeaseManager(): LeaseManager { return this.leaseManager; } /** * Get the list of unused configurations that were not accessed * @returns an array of unused configurations */ public getUnusedConfigs(configName: string): string[] { return this._configMaps.get(configName).getUnusedConfigs(); } public getK8Factory() { return this.k8Factory; } public getLocalConfig() { return this.localConfig; } public getRemoteConfigManager() { return this.remoteConfigManager; } abstract close(): Promise<void>; public commandActionBuilder(actionTasks: any, options: any, errorString: string, lease: Lease | null) { return async function (argv: any, commandDef: CommandHandlers) { const tasks = new Listr([...actionTasks], options); try { await tasks.run(); } catch (e: Error | any) { commandDef.parent.logger.error(`${errorString}: ${e.message}`, e); throw new SoloError(`${errorString}: ${e.message}`, e); } finally { const promises = []; promises.push(commandDef.parent.close()); if (lease) promises.push(lease.release()); await Promise.all(promises); } }; } /** * Setup home directories * @param dirs a list of directories that need to be created in sequence */ public setupHomeDirectory( dirs: string[] = [ constants.SOLO_HOME_DIR, constants.SOLO_LOGS_DIR, constants.SOLO_CACHE_DIR, constants.SOLO_VALUES_DIR, ], ) { const self = this; try { dirs.forEach(dirPath => { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, {recursive: true}); } self.logger.debug(`OK: setup directory: ${dirPath}`); }); } catch (e: Error | any) { this.logger.error(e); throw new SoloError(`failed to create directory: ${e.message}`, e); } return dirs; } public setupHomeDirectoryTask() { return new Task('Setup home directory', async () => { this.setupHomeDirectory(); }); } /** * Get the consensus nodes from the remoteConfigManager and use the localConfig to get the context * @returns an array of ConsensusNode objects */ public getConsensusNodes(): ConsensusNode[] { const consensusNodes: ConsensusNode[] = []; const clusters: Record<ClusterRef, Cluster> = this.getRemoteConfigManager().clusters; try { if (!this.getRemoteConfigManager()?.components?.consensusNodes) return []; } catch { return []; } // using the remoteConfigManager to get the consensus nodes if (this.getRemoteConfigManager()?.components?.consensusNodes) { Object.values(this.getRemoteConfigManager().components.consensusNodes).forEach(node => { consensusNodes.push( new ConsensusNode( node.name as NodeAlias, node.nodeId, node.namespace, node.cluster, // use local config to get the context this.getLocalConfig().clusterRefs[node.cluster], clusters[node.cluster].dnsBaseDomain, clusters[node.cluster].dnsConsensusNodePattern, Templates.renderConsensusNodeFullyQualifiedDomainName( node.name as NodeAlias, node.nodeId, node.namespace, node.cluster, clusters[node.cluster].dnsBaseDomain, clusters[node.cluster].dnsConsensusNodePattern, ), ), ); }); } // return the consensus nodes return consensusNodes; } /** * Gets a list of distinct contexts from the consensus nodes * @returns an array of context strings */ public getContexts(): string[] { const contexts: string[] = []; this.getConsensusNodes().forEach(node => { if (!contexts.includes(node.context)) { contexts.push(node.context); } }); return contexts; } /** * Gets a list of distinct cluster references from the consensus nodes * @returns an object of cluster references */ public getClusterRefs(): ClusterRefs { const clustersRefs: ClusterRefs = {}; this.getConsensusNodes().forEach(node => { if (!Object.keys(clustersRefs).includes(node.cluster)) { clustersRefs[node.cluster] = node.context; } }); return clustersRefs; } }