UNPKG

@hashgraph/solo

Version:

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

333 lines 13.2 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 { 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 { Flags } from './flags.js'; import { Templates } from '../core/templates.js'; export class BaseCommand extends ShellRunner { helm; k8Factory; chartManager; configManager; depManager; leaseManager; _configMaps = new Map(); localConfig; remoteConfigManager; constructor(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; } async prepareChartPath(chartDir, chartRepo, chartReleaseName) { 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 prepareValuesFiles(valuesFile) { 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, chartDirectory, profileValuesFile, valuesFileInput) { // initialize the map with an empty array for each cluster-ref const valuesFiles = { [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; } getConfigManager() { return this.configManager; } getChartManager() { 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. */ getConfig(configName, flags, extraProperties = []) { const configManager = this.configManager; // build the dynamic class that will keep track of which properties are used const NewConfigClass = class { usedConfigs; 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 = []; // 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; } getLeaseManager() { return this.leaseManager; } /** * Get the list of unused configurations that were not accessed * @returns an array of unused configurations */ getUnusedConfigs(configName) { return this._configMaps.get(configName).getUnusedConfigs(); } getK8Factory() { return this.k8Factory; } getLocalConfig() { return this.localConfig; } getRemoteConfigManager() { return this.remoteConfigManager; } commandActionBuilder(actionTasks, options, errorString, lease) { return async function (argv, commandDef) { const tasks = new Listr([...actionTasks], options); try { await tasks.run(); } catch (e) { 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 */ setupHomeDirectory(dirs = [ 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) { this.logger.error(e); throw new SoloError(`failed to create directory: ${e.message}`, e); } return dirs; } 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 */ getConsensusNodes() { const consensusNodes = []; const clusters = 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, 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, 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 */ getContexts() { const contexts = []; 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 */ getClusterRefs() { const clustersRefs = {}; this.getConsensusNodes().forEach(node => { if (!Object.keys(clustersRefs).includes(node.cluster)) { clustersRefs[node.cluster] = node.context; } }); return clustersRefs; } } //# sourceMappingURL=base.js.map