UNPKG

@hashgraph/solo

Version:

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

364 lines 17.2 kB
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