@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
333 lines • 13.2 kB
JavaScript
/**
* 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