UNPKG

@hashgraph/solo

Version:

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

503 lines 27.2 kB
// SPDX-License-Identifier: Apache-2.0 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); } }; var RemoteConfigRuntimeState_1; import { inject, injectable } from 'tsyringe-neo'; import { ReadRemoteConfigBeforeLoadError } from '../../../errors/read-remote-config-before-load-error.js'; import { WriteRemoteConfigBeforeLoadError } from '../../../errors/write-remote-config-before-load-error.js'; import { RemoteConfigSource } from '../../../../data/configuration/impl/remote-config-source.js'; import { YamlConfigMapStorageBackend } from '../../../../data/backend/impl/yaml-config-map-storage-backend.js'; import { InjectTokens } from '../../../../core/dependency-injection/inject-tokens.js'; import { patchInject } from '../../../../core/dependency-injection/container-helper.js'; import { NamespaceName } from '../../../../types/namespace/namespace-name.js'; import { ComponentStateMetadataSchema } from '../../../../data/schema/model/remote/state/component-state-metadata-schema.js'; import { Templates } from '../../../../core/templates.js'; import { DeploymentPhase } from '../../../../data/schema/model/remote/deployment-phase.js'; import { getSoloVersion } from '../../../../../version.js'; import * as constants from '../../../../core/constants.js'; import { SoloError } from '../../../../core/errors/solo-error.js'; import { Flags as flags } from '../../../../commands/flags.js'; import { promptTheUserForDeployment } from '../../../../core/resolvers.js'; import { ConsensusNode } from '../../../../core/model/consensus-node.js'; import { ComponentTypes } from '../../../../core/config/remote/enumerations/component-types.js'; import { LocalConfigRuntimeState } from '../local/local-config-runtime-state.js'; import { RemoteConfigMetadataSchema } from '../../../../data/schema/model/remote/remote-config-metadata-schema.js'; import { ApplicationVersionsSchema } from '../../../../data/schema/model/common/application-versions-schema.js'; import { ClusterSchema } from '../../../../data/schema/model/common/cluster-schema.js'; import { DeploymentStateSchema } from '../../../../data/schema/model/remote/deployment-state-schema.js'; import { DeploymentHistorySchema } from '../../../../data/schema/model/remote/deployment-history-schema.js'; import { RemoteConfigSchemaDefinition } from '../../../../data/schema/migration/impl/remote/remote-config-schema-definition.js'; import { RemoteConfigSchema } from '../../../../data/schema/model/remote/remote-config-schema.js'; import { ConsensusNodeStateSchema } from '../../../../data/schema/model/remote/state/consensus-node-state-schema.js'; import { RemoteConfig } from './remote-config.js'; import { ComponentIdsSchema } from '../../../../data/schema/model/remote/state/component-ids-schema.js'; import * as helpers from '../../../../core/helpers.js'; import { ResourceNotFoundError } from '../../../../integration/kube/errors/resource-operation-errors.js'; import { MissingRequiredParametersError } from '../../errors/missing-required-parameters-error.js'; import { SemanticVersion } from '../../../utils/semantic-version.js'; var RuntimeStatePhase; (function (RuntimeStatePhase) { RuntimeStatePhase["Loaded"] = "loaded"; RuntimeStatePhase["NotLoaded"] = "not_loaded"; })(RuntimeStatePhase || (RuntimeStatePhase = {})); let RemoteConfigRuntimeState = class RemoteConfigRuntimeState { static { RemoteConfigRuntimeState_1 = this; } k8Factory; logger; localConfig; configManager; remoteConfigValidator; objectMapper; static SOLO_REMOTE_CONFIGMAP_DATA_KEY = 'remote-config-data'; phase = RuntimeStatePhase.NotLoaded; clusterReferences = new Map(); namespace; source; backend; _remoteConfig; constructor(k8Factory, logger, localConfig, configManager, remoteConfigValidator, objectMapper) { this.k8Factory = k8Factory; this.logger = logger; this.localConfig = localConfig; this.configManager = configManager; this.remoteConfigValidator = remoteConfigValidator; this.objectMapper = objectMapper; this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name); this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name); this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name); this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name); this.remoteConfigValidator = patchInject(remoteConfigValidator, InjectTokens.RemoteConfigValidator, this.constructor.name); this.objectMapper = patchInject(objectMapper, InjectTokens.ObjectMapper, this.constructor.name); } get configuration() { this.failIfNotLoaded(); return this._remoteConfig; } get components() { this.failIfNotLoaded(); return this._remoteConfig.components; } get currentCluster() { return this.k8Factory.default().clusters().readCurrent(); } async load(namespace, context) { if (this.isLoaded()) { return; } await this.populateFromExisting(namespace, context); } async populateFromConfigMap(configMap, remoteConfig) { this.backend = new YamlConfigMapStorageBackend(configMap); this.source = new RemoteConfigSource(new RemoteConfigSchemaDefinition(this.objectMapper), this.objectMapper, this.backend); await this.source.load(); if (remoteConfig) { this.source.setModelData(remoteConfig); } this._remoteConfig = new RemoteConfig(this.source.modelData); this.phase = RuntimeStatePhase.Loaded; } async updateConfigMap(context, namespace, data) { await this.k8Factory.getK8(context).configMaps().update(namespace, constants.SOLO_REMOTE_CONFIGMAP_NAME, data); } isLoaded() { return this.phase === RuntimeStatePhase.Loaded; } failIfNotLoaded() { if (!this.isLoaded()) { throw new ReadRemoteConfigBeforeLoadError('Attempting to read from remote config before loading it'); } } async persist() { if (!this.isLoaded()) { throw new WriteRemoteConfigBeforeLoadError('Attempting to persist remote config before loading it'); } await this.source.persist(); const remoteConfigDataBytes = await this.backend.readBytes(RemoteConfigRuntimeState_1.SOLO_REMOTE_CONFIGMAP_DATA_KEY); const remoteConfigData = { [RemoteConfigRuntimeState_1.SOLO_REMOTE_CONFIGMAP_DATA_KEY]: remoteConfigDataBytes.toString('utf8'), }; const promises = []; for (const context of this.clusterReferences.keys()) { promises.push(this.updateConfigMap(context, this.namespace, remoteConfigData)); } await Promise.all(promises); } async create(argv, ledgerPhase, nodeAliases, namespace, deploymentName, clusterReference, context, dnsBaseDomain, dnsConsensusNodePattern) { this.populateClusterReferences(deploymentName); const consensusNodeStates = nodeAliases.map((nodeAlias) => { return new ConsensusNodeStateSchema(new ComponentStateMetadataSchema(Templates.renderComponentIdFromNodeAlias(nodeAlias), namespace.name, clusterReference, DeploymentPhase.REQUESTED)); }); const userIdentity = this.localConfig.configuration.userIdentity; const cliVersion = new SemanticVersion(getSoloVersion()); const command = argv._.join(' '); const cluster = new ClusterSchema(clusterReference, namespace.name, deploymentName, dnsBaseDomain, dnsConsensusNodePattern); const remoteConfig = new RemoteConfigSchema(6, new RemoteConfigMetadataSchema(new Date(), userIdentity), new ApplicationVersionsSchema(cliVersion), [cluster], new DeploymentStateSchema(ledgerPhase, new ComponentIdsSchema(nodeAliases.length + 1), consensusNodeStates), new DeploymentHistorySchema([command], command)); const configMap = await this.createConfigMap(namespace, context); await this.populateFromConfigMap(configMap, remoteConfig); await this.persist(); } async createFromExisting(namespace, clusterReference, deploymentName, componentFactory, dnsBaseDomain, dnsConsensusNodePattern, existingClusterContext, argv, nodeAliases) { await this.populateFromExisting(namespace, existingClusterContext); this.populateClusterReferences(deploymentName); const newClusterContext = this.localConfig.configuration.clusterRefs .get(clusterReference.toString()) ?.toString(); //? Create copy of the existing remote config inside the new cluster await this.createConfigMap(namespace, newClusterContext); await this.persist(); //* update the command history this.addCommandToHistory(argv._.join(' ')); //* add the new clusters this.configuration.addCluster(new ClusterSchema(clusterReference, namespace.name, deploymentName, dnsBaseDomain, dnsConsensusNodePattern)); //* add the new nodes to components for (const nodeAlias of nodeAliases) { this.configuration.components.addNewComponent(componentFactory.createNewConsensusNodeComponent(Templates.renderComponentIdFromNodeAlias(nodeAlias), clusterReference, namespace, DeploymentPhase.REQUESTED), ComponentTypes.ConsensusNode); } await this.persist(); } addCommandToHistory(command) { this.source.modelData.history.commands.push(command); this.source.modelData.history.lastExecutedCommand = command; if (this.source.modelData.history.commands.length > constants.SOLO_REMOTE_CONFIG_MAX_COMMAND_IN_HISTORY) { this.source.modelData.history.commands.shift(); } } async createConfigMap(namespace, context) { const name = constants.SOLO_REMOTE_CONFIGMAP_NAME; const labels = constants.SOLO_REMOTE_CONFIGMAP_LABELS; await this.k8Factory .getK8(context) .configMaps() .create(namespace, name, labels, { [RemoteConfigRuntimeState_1.SOLO_REMOTE_CONFIGMAP_DATA_KEY]: '{}' }); return await this.k8Factory.getK8(context).configMaps().read(namespace, name); } async getConfigMap(namespace, context) { if (!namespace || !context) { throw new MissingRequiredParametersError(`Namespace and context are required to get the remote config ConfigMap, received namespace: ${namespace}, context: ${context}`); } let configMap; try { configMap = await this.k8Factory .getK8(context) .configMaps() .read(namespace, constants.SOLO_REMOTE_CONFIGMAP_NAME); } catch (error) { throw error instanceof ResourceNotFoundError ? error : new SoloError(`Failed to get remote config ConfigMap for namespace: ${namespace}, context: ${context}. Error: ${error.message}`, error); } if (!configMap) { throw new SoloError(`Remote config ConfigMap not found for namespace: ${namespace}, context: ${context}`); } return configMap; } async populateFromExisting(namespace, context) { const remoteConfigConfigMap = await this.getConfigMap(namespace, context); await this.populateFromConfigMap(remoteConfigConfigMap); } async remoteConfigExists(namespace, context) { const configMap = await this.getConfigMap(namespace, context); return !!configMap; } populateClusterReferences(deploymentName) { let deployment; try { deployment = this.localConfig.configuration.deploymentByName(deploymentName); } catch { // Deployment not in local config — fall back to namespace/context already resolved from remote config scan. const namespaceFromConfig = this.configManager.getFlag(flags.namespace); if (namespaceFromConfig) { this.namespace = namespaceFromConfig; } return this.configManager.getFlag(flags.context); } this.namespace = NamespaceName.of(deployment.namespace); for (const clusterReference of deployment.clusters) { const context = this.localConfig.configuration.clusterRefs.get(clusterReference.toString())?.toString(); this.clusterReferences.set(context, clusterReference.toString()); } return this.localConfig.configuration.clusterRefs.get(deployment.clusters.get(0)?.toString())?.toString(); } /** * 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. * @param validate - whether to validate the remote configuration. * @param [skipConsensusNodesValidation] - whether or not to validate the consensusNodes */ async loadAndValidate(argv, validate = true, skipConsensusNodesValidation = true) { await this.setDefaultNamespaceAndDeploymentIfNotSet(argv); await this.setDefaultContextIfNotSet(); // Sync resolved context back to argv so subsequent configManager.update(argv) preserves it. argv[flags.context.name] ||= this.configManager.getFlag(flags.context); const deploymentName = this.configManager.getFlag(flags.deployment); const context = this.populateClusterReferences(deploymentName); // TODO: Compare configs from clusterReferences await this.load(this.namespace, context); this.logger.info('Remote config loaded'); if (!validate) { return; } await this.remoteConfigValidator.validateComponents(this.namespace, skipConsensusNodesValidation, this.configuration.state); const currentCommand = argv._?.join(' '); const commandArguments = flags.stringifyArgv(argv); this.addCommandToHistory(`Executed by ${this.localConfig.configuration.userIdentity.name}: ${currentCommand} ${commandArguments}`.trim()); this.initializeComponentVersions(argv, this.source.modelData); await this.persist(); } initializeComponentVersions(argv, remoteConfig) { remoteConfig.versions.chart = argv[flags.soloChartVersion.name] ? new SemanticVersion(argv[flags.soloChartVersion.name]) : new SemanticVersion(flags.soloChartVersion.definition.defaultValue); // set default versions if not set const componentTypes = [ ComponentTypes.BlockNode, ComponentTypes.RelayNodes, ComponentTypes.MirrorNode, ComponentTypes.Explorer, ComponentTypes.ConsensusNode, ]; for (const componentType of componentTypes) { const version = this.getComponentVersion(componentType); if (version.equals('0.0.0')) { switch (componentType) { case ComponentTypes.BlockNode: { this.updateComponentVersion(componentType, new SemanticVersion(flags.blockNodeChartVersion.definition.defaultValue)); break; } case ComponentTypes.RelayNodes: { this.updateComponentVersion(componentType, new SemanticVersion(flags.relayReleaseTag.definition.defaultValue)); break; } case ComponentTypes.MirrorNode: { this.updateComponentVersion(componentType, new SemanticVersion(flags.mirrorNodeVersion.definition.defaultValue)); break; } case ComponentTypes.Explorer: { this.updateComponentVersion(componentType, new SemanticVersion(flags.explorerVersion.definition.defaultValue)); break; } case ComponentTypes.ConsensusNode: { this.updateComponentVersion(componentType, new SemanticVersion(flags.releaseTag.definition.defaultValue)); break; } default: { throw new SoloError(`Unsupported component type: ${componentType}`); } } } } } async deleteComponents() { this._remoteConfig.state.consensusNodes = []; this._remoteConfig.state.blockNodes = []; this._remoteConfig.state.envoyProxies = []; this._remoteConfig.state.haProxies = []; this._remoteConfig.state.explorers = []; this._remoteConfig.state.mirrorNodes = []; this._remoteConfig.state.relayNodes = []; await this.persist(); } async setDefaultNamespaceAndDeploymentIfNotSet(argv) { let namespaceFromConfig = this.configManager.getFlag(flags.namespace); let deploymentName = this.configManager.getFlag(flags.deployment); const deploymentFromArgv = argv[flags.deployment.name]; const namespaceFromArgv = argv[flags.namespace.name]; // Keep config manager in sync when deployment/namespace are resolved directly in argv by caller logic. if (!deploymentName && deploymentFromArgv) { this.configManager.setFlag(flags.deployment, deploymentFromArgv); deploymentName = deploymentFromArgv; } // Keep config manager in sync when namespace is resolved directly in argv by caller logic. // argv takes precedence over the default namespace that middleware may have set from kubectl context. if (namespaceFromArgv) { this.configManager.setFlag(flags.namespace, namespaceFromArgv); namespaceFromConfig = namespaceFromArgv; } if (namespaceFromConfig && deploymentName) { return; } // TODO: Current quick fix for commands where namespace is not passed let currentDeployment = this.localConfig.configuration.deploymentByName(deploymentName); if (!deploymentName) { deploymentName = await promptTheUserForDeployment(this.configManager); currentDeployment = this.localConfig.configuration.deploymentByName(deploymentName); // TODO: Fix once we have the DataManager, // without this the user will be prompted a second time for the deployment // TODO: we should not be mutating argv argv[flags.deployment.name] = deploymentName; this.logger.warn(`Deployment name not found in flags or local config, setting it in argv and config manager to: ${deploymentName}`); this.configManager.setFlag(flags.deployment, deploymentName); } if (!currentDeployment) { throw new SoloError(`Selected deployment name is not set in local config - ${deploymentName}`); } const namespace = currentDeployment.namespace; this.logger.warn(`Namespace not found in flags, setting it to: ${namespace}`); this.configManager.setFlag(flags.namespace, namespace); argv[flags.namespace.name] = namespace; } async setDefaultContextIfNotSet() { if (this.configManager.hasFlag(flags.context)) { return; } let context; try { context = await this.getContextForFirstCluster(); } catch { context = this.k8Factory.default().contexts().readCurrent(); } if (!context) { throw new SoloError("Context is not passed and default one can't be acquired"); } this.logger.warn(`Context not found in flags, setting it to: ${context}`); this.configManager.setFlag(flags.context, context); } //* Common Commands /** * Get the consensus nodes from the remoteConfig and use the localConfig to get the context * @returns an array of ConsensusNode objects */ getConsensusNodes() { if (!this.isLoaded()) { throw new SoloError('Remote configuration is not loaded, and was expected to be loaded'); } const consensusNodes = []; for (const node of Object.values(this.configuration.state.consensusNodes)) { const cluster = this.configuration.clusters.find((cluster) => cluster.name === node.metadata.cluster); const context = this.localConfig.configuration.clusterRefs.get(node.metadata.cluster)?.toString() ?? this.configManager.getFlag(flags.context); const nodeAlias = Templates.renderNodeAliasFromNumber(node.metadata.id); const nodeId = Templates.renderNodeIdFromComponentId(node.metadata.id); consensusNodes.push(new ConsensusNode(nodeAlias, nodeId, node.metadata.namespace, node.metadata.cluster, context, cluster.dnsBaseDomain, cluster.dnsConsensusNodePattern, Templates.renderConsensusNodeFullyQualifiedDomainName(nodeAlias, nodeId, node.metadata.namespace, node.metadata.cluster, cluster.dnsBaseDomain, cluster.dnsConsensusNodePattern), node.blockNodeMap, node.externalBlockNodeMap)); } // return the consensus nodes return consensusNodes; } /** * Gets a list of distinct contexts from the consensus nodes. * @returns an array of context strings. */ getContexts() { return [...new Set(this.getConsensusNodes().map((node) => node.context))]; } /** * Gets a list of distinct cluster references from the consensus nodes. * @returns an object of cluster references. */ getClusterRefs() { const nodes = this.getConsensusNodes(); const accumulator = new Map(); for (const node of nodes) { accumulator.set(node.cluster, node.context); } return accumulator; } async getContextForFirstCluster() { const deploymentName = this.configManager.getFlag(flags.deployment); const clusterReference = this.localConfig.configuration.deploymentByName(deploymentName)?.clusters?.get(0)?.toString() ?? this.k8Factory.default().clusters().readCurrent(); const context = this.localConfig.configuration.clusterRefs.get(clusterReference)?.toString(); this.logger.debug(`Using context ${context} for cluster ${clusterReference} for deployment ${deploymentName}`); return context; } getNamespace() { return NamespaceName.of(this.configuration.clusters?.at(0)?.namespace); } extractContextFromConsensusNodes(nodeAlias) { return helpers.extractContextFromConsensusNodes(nodeAlias, this.getConsensusNodes()); } updateComponentVersion(type, version) { const updateVersionCallback = (versionField) => { versionField.value = version; }; this.applyCallbackToVersionField(type, updateVersionCallback); } /** * Method used to map the component type to the specific version field * and pass it to a callback to apply modifications */ applyCallbackToVersionField(componentType, callback) { switch (componentType) { case ComponentTypes.ConsensusNode: { const versionField = { value: this.configuration.versions.consensusNode }; callback(versionField); this.configuration.versions.consensusNode = versionField.value; break; } case ComponentTypes.MirrorNode: { const versionField = { value: this.configuration.versions.mirrorNodeChart }; callback(versionField); this.configuration.versions.mirrorNodeChart = versionField.value; break; } case ComponentTypes.Explorer: { const versionField = { value: this.configuration.versions.explorerChart }; callback(versionField); this.configuration.versions.explorerChart = versionField.value; break; } case ComponentTypes.RelayNodes: { const versionField = { value: this.configuration.versions.jsonRpcRelayChart }; callback(versionField); this.configuration.versions.jsonRpcRelayChart = versionField.value; break; } case ComponentTypes.BlockNode: { const versionField = { value: this.configuration.versions.blockNodeChart }; callback(versionField); this.configuration.versions.blockNodeChart = versionField.value; break; } case ComponentTypes.Cli: { const versionField = { value: this.configuration.versions.cli }; callback(versionField); this.configuration.versions.cli = versionField.value; break; } case ComponentTypes.Chart: { const versionField = { value: this.configuration.versions.chart }; callback(versionField); this.configuration.versions.chart = versionField.value; break; } default: { throw new SoloError(`Unsupported component type: ${componentType}`); } } } getComponentVersion(type) { let version; const getVersionCallback = (versionField) => { version = versionField.value; }; this.applyCallbackToVersionField(type, getVersionCallback); return version; } }; RemoteConfigRuntimeState = RemoteConfigRuntimeState_1 = __decorate([ injectable(), __param(0, inject(InjectTokens.K8Factory)), __param(1, inject(InjectTokens.SoloLogger)), __param(2, inject(InjectTokens.LocalConfigRuntimeState)), __param(3, inject(InjectTokens.ConfigManager)), __param(4, inject(InjectTokens.RemoteConfigValidator)), __param(5, inject(InjectTokens.ObjectMapper)), __metadata("design:paramtypes", [Object, Object, LocalConfigRuntimeState, Function, Object, Object]) ], RemoteConfigRuntimeState); export { RemoteConfigRuntimeState }; //# sourceMappingURL=remote-config-runtime-state.js.map