@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
364 lines • 17.2 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
/**
* SPDX-License-Identifier: Apache-2.0
*/
import * as constants from '../../constants.js';
import { MissingArgumentError, SoloError } from '../../errors.js';
import { RemoteConfigDataWrapper } from './remote_config_data_wrapper.js';
import chalk from 'chalk';
import { RemoteConfigMetadata } from './metadata.js';
import { Flags as flags } from '../../../commands/flags.js';
import * as yaml from 'yaml';
import { ComponentsDataWrapper } from './components_data_wrapper.js';
import { RemoteConfigValidator } from './remote_config_validator.js';
import { inject, injectable } from 'tsyringe-neo';
import { patchInject } from '../../dependency_injection/container_helper.js';
import { ErrorMessages } from '../../error_messages.js';
import { CommonFlagsDataWrapper } from './common_flags_data_wrapper.js';
import { NamespaceName } from '../../kube/resources/namespace/namespace_name.js';
import { ResourceNotFoundError } from '../../kube/errors/resource_operation_errors.js';
import { InjectTokens } from '../../dependency_injection/inject_tokens.js';
import { Cluster } from './cluster.js';
import * as helpers from '../../helpers.js';
/**
* Uses Kubernetes ConfigMaps to manage the remote configuration data by creating, loading, modifying,
* and saving the configuration data to and from a Kubernetes cluster.
*/
let RemoteConfigManager = class RemoteConfigManager {
k8Factory;
logger;
localConfig;
configManager;
/** Stores the loaded remote configuration data. */
remoteConfig;
/**
* @param k8Factory - The Kubernetes client used for interacting with ConfigMaps.
* @param logger - The logger for recording activity and errors.
* @param localConfig - Local configuration for the remote config.
* @param configManager - Manager to retrieve application flags and settings.
*/
constructor(k8Factory, logger, localConfig, configManager) {
this.k8Factory = k8Factory;
this.logger = logger;
this.localConfig = localConfig;
this.configManager = configManager;
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
this.localConfig = patchInject(localConfig, InjectTokens.LocalConfig, this.constructor.name);
this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
}
/* ---------- Getters ---------- */
get currentCluster() {
return this.k8Factory.default().clusters().readCurrent();
}
/** @returns the components data wrapper cloned */
get components() {
return this.remoteConfig?.components?.clone();
}
/**
* @returns the remote configuration data's clusters cloned
*/
get clusters() {
return Object.assign({}, this.remoteConfig?.clusters);
}
/* ---------- Readers and Modifiers ---------- */
/**
* Modifies the loaded remote configuration data using a provided callback function.
* The callback operates on the configuration data, which is then saved to the cluster.
*
* @param callback - an async function that modifies the remote configuration data.
* @throws {@link SoloError} if the configuration is not loaded before modification.
*/
async modify(callback) {
if (!this.remoteConfig) {
return;
// TODO see if this should be disabled to make it an optional feature
// throw new SoloError('Attempting to modify remote config without loading it first')
}
await callback(this.remoteConfig);
await this.save();
}
/**
* Creates a new remote configuration in the Kubernetes cluster.
* Gathers data from the local configuration and constructs a new ConfigMap
* entry in the cluster with initial command history and metadata.
*/
async create(argv) {
const clusters = {};
Object.entries(this.localConfig.deployments).forEach(([deployment, deploymentStructure]) => {
const namespace = deploymentStructure.namespace.toString();
deploymentStructure.clusters.forEach(cluster => (clusters[cluster] = new Cluster(cluster, namespace, deployment)));
});
// temporary workaround until we can have `solo deployment add` command
const nodeAliases = helpers.splitFlagInput(this.configManager.getFlag(flags.nodeAliasesUnparsed));
this.remoteConfig = new RemoteConfigDataWrapper({
metadata: new RemoteConfigMetadata(this.getNamespace().name, this.configManager.getFlag(flags.deployment), new Date(), this.localConfig.userEmailAddress, helpers.getSoloVersion()),
clusters,
commandHistory: ['deployment create'],
lastExecutedCommand: 'deployment create',
components: ComponentsDataWrapper.initializeWithNodes(nodeAliases, this.configManager.getFlag(flags.deploymentClusters), this.getNamespace().name),
flags: await CommonFlagsDataWrapper.initialize(this.configManager, argv),
});
await this.createConfigMap();
}
/**
* Saves the currently loaded remote configuration data to the Kubernetes cluster.
* @throws {@link SoloError} if there is no remote configuration data to save.
*/
async save() {
if (!this.remoteConfig) {
throw new SoloError('Attempted to save remote config without data');
}
await this.replaceConfigMap();
}
/**
* Loads the remote configuration from the Kubernetes cluster if it exists.
* @returns true if the configuration is loaded successfully.
*/
async load() {
if (this.remoteConfig)
return true;
try {
const configMap = await this.getConfigMap();
if (configMap) {
this.remoteConfig = RemoteConfigDataWrapper.fromConfigmap(this.configManager, configMap);
return true;
}
return false;
}
catch {
return false;
}
}
/**
* Loads the remote configuration, performs a validation and returns it
* @returns RemoteConfigDataWrapper
*/
async get() {
await this.load();
try {
await RemoteConfigValidator.validateComponents(this.configManager.getFlag(flags.namespace), this.remoteConfig.components, this.k8Factory, this.localConfig);
}
catch {
throw new SoloError(ErrorMessages.REMOTE_CONFIG_IS_INVALID(this.k8Factory.default().clusters().readCurrent()));
}
return this.remoteConfig;
}
static compare(remoteConfig1, remoteConfig2) {
// Compare clusters
const clusters1 = Object.keys(remoteConfig1.clusters);
const clusters2 = Object.keys(remoteConfig2.clusters);
if (clusters1.length !== clusters2.length)
return false;
for (const i in clusters1) {
if (clusters1[i] !== clusters2[i]) {
return false;
}
}
return true;
}
/* ---------- Listr Task Builders ---------- */
/**
* Performs the loading of the remote configuration.
* Checks if the configuration is already loaded, otherwise loads and adds the command to history.
*
* @param argv - arguments containing command input for historical reference.
*/
async loadAndValidate(argv) {
const self = this;
try {
self.setDefaultNamespaceIfNotSet();
self.setDefaultContextIfNotSet();
}
catch (e) {
self.logger.showUser(chalk.red(e.message));
return;
}
if (!(await self.load())) {
self.logger.showUser(chalk.red('remote config not found'));
// TODO see if this should be disabled to make it an optional feature
return;
// throw new SoloError('Failed to load remote config')
}
await RemoteConfigValidator.validateComponents(this.configManager.getFlag(flags.namespace), self.remoteConfig.components, self.k8Factory, this.localConfig);
const additionalCommandData = `Executed by ${self.localConfig.userEmailAddress}: `;
const currentCommand = argv._?.join(' ');
const commandArguments = flags.stringifyArgv(argv);
self.remoteConfig.addCommandToHistory(additionalCommandData + (currentCommand + ' ' + commandArguments).trim());
self.populateVersionsInMetadata(argv);
await self.remoteConfig.flags.handleFlags(argv);
await self.save();
}
populateVersionsInMetadata(argv) {
const command = argv._?.[0];
const subcommand = argv._?.[1];
const isCommandUsingSoloChartVersionFlag = (command === 'network' && subcommand === 'deploy') ||
(command === 'network' && subcommand === 'refresh') ||
(command === 'node' && subcommand === 'update') ||
(command === 'node' && subcommand === 'update-execute') ||
(command === 'node' && subcommand === 'add') ||
(command === 'node' && subcommand === 'add-execute') ||
(command === 'node' && subcommand === 'delete') ||
(command === 'node' && subcommand === 'delete-execute');
if (argv[flags.soloChartVersion.constName]) {
this.remoteConfig.metadata.soloChartVersion = argv[flags.soloChartVersion.constName];
}
else if (isCommandUsingSoloChartVersionFlag) {
this.remoteConfig.metadata.soloChartVersion = flags.soloChartVersion.definition.defaultValue;
}
const isCommandUsingReleaseTagVersionFlag = (command === 'node' && subcommand !== 'keys' && subcommand !== 'logs' && subcommand !== 'states') ||
(command === 'network' && subcommand === 'deploy');
if (argv[flags.releaseTag.constName]) {
this.remoteConfig.metadata.hederaPlatformVersion = argv[flags.releaseTag.constName];
}
else if (isCommandUsingReleaseTagVersionFlag) {
this.remoteConfig.metadata.hederaPlatformVersion = flags.releaseTag.definition.defaultValue;
}
if (argv[flags.mirrorNodeVersion.constName]) {
this.remoteConfig.metadata.hederaMirrorNodeChartVersion = argv[flags.mirrorNodeVersion.constName];
}
else if (command === 'mirror-node' && subcommand === 'deploy') {
this.remoteConfig.metadata.hederaMirrorNodeChartVersion = flags.mirrorNodeVersion.definition
.defaultValue;
}
if (argv[flags.hederaExplorerVersion.constName]) {
this.remoteConfig.metadata.hederaExplorerChartVersion = argv[flags.hederaExplorerVersion.constName];
}
else if (command === 'explorer' && subcommand === 'deploy') {
this.remoteConfig.metadata.hederaExplorerChartVersion = flags.hederaExplorerVersion.definition
.defaultValue;
}
if (argv[flags.relayReleaseTag.constName]) {
this.remoteConfig.metadata.hederaJsonRpcRelayChartVersion = argv[flags.relayReleaseTag.constName];
}
else if (command === 'relay' && subcommand === 'deploy') {
this.remoteConfig.metadata.hederaJsonRpcRelayChartVersion = flags.relayReleaseTag.definition
.defaultValue;
}
}
async createAndValidate(clusterRef, context, namespace, argv) {
const self = this;
self.k8Factory.default().contexts().updateCurrent(context);
if (!(await self.k8Factory.default().namespaces().has(NamespaceName.of(namespace)))) {
await self.k8Factory.default().namespaces().create(NamespaceName.of(namespace));
}
const localConfigExists = this.localConfig.configFileExists();
if (!localConfigExists) {
throw new SoloError("Local config doesn't exist");
}
self.unload();
if (await self.load()) {
self.logger.showUser(chalk.red('Remote config already exists'));
throw new SoloError('Remote config already exists');
}
await self.create(argv);
}
/* ---------- Utilities ---------- */
/** Empties the component data inside the remote config */
async deleteComponents() {
await this.modify(async (remoteConfig) => {
remoteConfig.components = ComponentsDataWrapper.initializeEmpty();
});
}
isLoaded() {
return !!this.remoteConfig;
}
unload() {
delete this.remoteConfig;
}
/**
* Retrieves the ConfigMap containing the remote configuration from the Kubernetes cluster.
*
* @returns the remote configuration data.
* @throws {@link SoloError} if the ConfigMap could not be read and the error is not a 404 status.
*/
async getConfigMap() {
try {
return await this.k8Factory
.default()
.configMaps()
.read(this.getNamespace(), constants.SOLO_REMOTE_CONFIGMAP_NAME);
}
catch (error) {
if (!(error instanceof ResourceNotFoundError)) {
throw new SoloError('Failed to read remote config from cluster', error);
}
return null;
}
}
/**
* Creates a new ConfigMap entry in the Kubernetes cluster with the remote configuration data.
*/
async createConfigMap() {
await this.k8Factory
.default()
.configMaps()
.create(this.getNamespace(), constants.SOLO_REMOTE_CONFIGMAP_NAME, constants.SOLO_REMOTE_CONFIGMAP_LABELS, {
'remote-config-data': yaml.stringify(this.remoteConfig.toObject()),
});
}
/** Replaces an existing ConfigMap in the Kubernetes cluster with the current remote configuration data. */
async replaceConfigMap() {
await this.k8Factory
.default()
.configMaps()
.replace(this.getNamespace(), constants.SOLO_REMOTE_CONFIGMAP_NAME, constants.SOLO_REMOTE_CONFIGMAP_LABELS, {
'remote-config-data': yaml.stringify(this.remoteConfig.toObject()),
});
}
setDefaultNamespaceIfNotSet() {
if (this.configManager.hasFlag(flags.namespace))
return;
// TODO: Current quick fix for commands where namespace is not passed
const deploymentName = this.configManager.getFlag(flags.deployment);
const currentDeployment = this.localConfig.deployments[deploymentName];
if (!this.localConfig?.deployments[deploymentName]) {
this.logger.error('Selected deployment name is not set in local config', this.localConfig);
throw new SoloError('Selected deployment name is not set in local config');
}
const namespace = currentDeployment.namespace;
this.configManager.setFlag(flags.namespace, namespace);
}
setDefaultContextIfNotSet() {
if (this.configManager.hasFlag(flags.context))
return;
const context = this.k8Factory.default().contexts().readCurrent();
if (!context) {
this.logger.error("Context is not passed and default one can't be acquired", this.localConfig);
throw new SoloError("Context is not passed and default one can't be acquired");
}
this.configManager.setFlag(flags.context, context);
}
// cluster will be retrieved from LocalConfig based the context to cluster mapping
/**
* Retrieves the namespace value from the configuration manager's flags.
* @returns string - The namespace value if set.
*/
getNamespace() {
const ns = this.configManager.getFlag(flags.namespace);
if (!ns)
throw new MissingArgumentError('namespace is not set');
return ns;
}
};
RemoteConfigManager = __decorate([
injectable(),
__param(0, inject(InjectTokens.K8Factory)),
__param(1, inject(InjectTokens.SoloLogger)),
__param(2, inject(InjectTokens.LocalConfig)),
__param(3, inject(InjectTokens.ConfigManager)),
__metadata("design:paramtypes", [Object, Function, Function, Function])
], RemoteConfigManager);
export { RemoteConfigManager };
//# sourceMappingURL=remote_config_manager.js.map