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