@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
284 lines • 14.3 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { SoloError } from '../../errors/solo-error.js';
import { ComponentTypes } from './enumerations/component-types.js';
import { BaseStateSchema } from '../../../data/schema/model/remote/state/base-state-schema.js';
import { isValidEnum } from '../../util/validation-helpers.js';
import * as constants from '../../constants.js';
import { Templates } from '../../templates.js';
export class ComponentsDataWrapper {
state;
constructor(state) {
this.state = state;
}
get componentIds() {
return this.state.componentIds;
}
/* -------- Modifiers -------- */
/** Used to add new component to their respective group. */
addNewComponent(component, type, isReplace, skipIncrement = false) {
const componentId = component.metadata.id;
if (typeof componentId !== 'number') {
throw new SoloError(`Component id is required ${componentId}`);
}
if (!(component instanceof BaseStateSchema)) {
throw new SoloError('Component must be instance of BaseState', undefined, BaseStateSchema);
}
const addComponentCallback = (components) => {
if (this.checkComponentExists(components, component) && !isReplace) {
throw new SoloError('Component exists', undefined, component);
}
components.push(component);
};
this.applyCallbackToComponentGroup(type, addComponentCallback, componentId);
if (!skipIncrement) {
// Increment the component id counter for the specified type when adding
this.componentIds[type] += 1;
}
}
// TODO: Remove once unified method is fully utilized
changeNodePhase(componentId, phase) {
if (!this.state.consensusNodes.some((component) => +component.metadata.id === +componentId)) {
throw new SoloError(`Consensus node ${componentId} doesn't exist`);
}
const component = this.state.consensusNodes.find((component) => +component.metadata.id === +componentId);
component.metadata.phase = phase;
}
changeComponentPhase(componentId, type, phase) {
if (typeof componentId !== 'number') {
throw new SoloError(`Component id is required ${componentId}`);
}
const updateComponentCallback = (components) => {
const component = components.find((component) => component.metadata.id === componentId);
if (!component) {
throw new SoloError(`Component ${componentId} of type ${type} not found while attempting to update`);
}
component.metadata.phase = phase;
};
this.applyCallbackToComponentGroup(type, updateComponentCallback, componentId);
}
/** Used to remove specific component from their respective group. */
removeComponent(componentId, type) {
if (typeof componentId !== 'number') {
throw new SoloError(`Component id is required ${componentId}`);
}
if (!isValidEnum(type, ComponentTypes)) {
throw new SoloError(`Invalid component type ${type}`);
}
const removeComponentCallback = (components) => {
const index = components.findIndex((component) => component.metadata.id === componentId);
if (index === -1) {
throw new SoloError(`Component ${componentId} of type ${type} not found while attempting to remove`);
}
components.splice(index, 1);
};
this.applyCallbackToComponentGroup(type, removeComponentCallback, componentId);
}
/* -------- Utilities -------- */
getComponent(type, componentId) {
let component;
const getComponentCallback = (components) => {
component = components.find((component) => component.metadata.id === componentId);
if (!component) {
throw new SoloError(`Component ${componentId} of type ${type} not found while attempting to read`);
}
};
this.applyCallbackToComponentGroup(type, getComponentCallback, componentId);
return component;
}
getComponentByType(type) {
let components = [];
const getComponentsByTypeCallback = (comps) => {
components = comps;
};
this.applyCallbackToComponentGroup(type, getComponentsByTypeCallback);
return components;
}
getComponentsByClusterReference(type, clusterReference) {
let filteredComponents = [];
const getComponentsByClusterReferenceCallback = (components) => {
filteredComponents = components.filter((component) => component.metadata.cluster === clusterReference);
};
this.applyCallbackToComponentGroup(type, getComponentsByClusterReferenceCallback);
return filteredComponents;
}
getComponentById(type, id) {
let filteredComponent;
const getComponentByIdCallback = (components) => {
filteredComponent = components.find((component) => +component.metadata.id === +id);
};
this.applyCallbackToComponentGroup(type, getComponentByIdCallback);
if (!filteredComponent) {
throw new SoloError(`Component of type ${type} with id ${id} was not found in remote config`);
}
return filteredComponent;
}
/**
* Method used to map the type to the specific component group
* and pass it to a callback to apply modifications
*/
applyCallbackToComponentGroup(componentType, callback, componentId) {
switch (componentType) {
case ComponentTypes.RelayNodes: {
callback(this.state.relayNodes);
break;
}
case ComponentTypes.HaProxy: {
callback(this.state.haProxies);
break;
}
case ComponentTypes.MirrorNode: {
callback(this.state.mirrorNodes);
break;
}
case ComponentTypes.EnvoyProxy: {
callback(this.state.envoyProxies);
break;
}
case ComponentTypes.ConsensusNode: {
callback(this.state.consensusNodes);
break;
}
case ComponentTypes.Explorer: {
callback(this.state.explorers);
break;
}
case ComponentTypes.BlockNode: {
callback(this.state.blockNodes);
break;
}
case ComponentTypes.Postgres: {
callback(this.state.postgres);
break;
}
case ComponentTypes.Redis: {
callback(this.state.redis);
break;
}
default: {
throw new SoloError(`Unknown component type ${componentType}, component id: ${componentId}`);
}
}
}
/** checks if component exists in the respective group */
checkComponentExists(components, newComponent) {
return components.some((component) => component.metadata.id === newComponent.metadata.id);
}
getNewComponentId(componentType) {
return this.componentIds[componentType];
}
/**
* Manages port forwarding for a component, checking if it's already enabled and persisting configuration
* @param clusterReference The cluster reference to forward to
* @param podReference The pod reference to forward to
* @param podPort The port on the pod to forward from
* @param localPort The local port to forward to (starting port if not available)
* @param k8Client The Kubernetes client to use for port forwarding
* @param logger Logger for messages
* @param componentType The component type for persistence
* @param label Label for the port forward
* @param reuse Whether to reuse existing port forward if available
* @param nodeId Optional node ID for finding component when cluster reference is not available
* @returns The local port number that was used for port forwarding
*/
async managePortForward(clusterReference, podReference, podPort, localPort, k8Client, logger, componentType, label, reuse = false, nodeId, persist = false, externalAddress) {
// found component by cluster reference or nodeId
let component;
if (clusterReference) {
const schemeComponents = this.getComponentsByClusterReference(componentType, clusterReference);
component = schemeComponents[0];
}
else {
const componentId = Templates.renderComponentIdFromNodeId(nodeId);
component = this.getComponentById(componentType, componentId);
}
if (component === undefined) {
// it is possible we are upgrading a chart and previous version has no clusterReference save in configMap
// so we will not be able to find component by clusterReference
reuse = true;
logger.showUser(`Port forward config not found for previous installed ${label}, reusing existing port forward`);
}
else if (component.metadata.portForwardConfigs) {
for (const portForwardConfig of component.metadata.portForwardConfigs) {
if (reuse === true && portForwardConfig.podPort === podPort) {
if (portForwardConfig.localPort === localPort) {
logger.showUser(`${label} Port forward already enabled at ${portForwardConfig.localPort}`);
return portForwardConfig.localPort;
}
// localPort changed (migration) — kill the old process so portForward() reuse logic
// does not find it and return the stale port, then remove the stale config.
logger.showUser(`${label} Port forward migrating from ${portForwardConfig.localPort} to ${localPort}`);
// eslint-disable-next-line unicorn/no-null
await k8Client.pods().readByReference(null).stopPortForward(portForwardConfig.localPort);
component.metadata.portForwardConfigs = component.metadata.portForwardConfigs.filter((c) => !(c.podPort === podPort && c.localPort === portForwardConfig.localPort));
break;
}
}
}
// Enable port forwarding
const portForwardPortNumber = await k8Client
.pods()
.readByReference(podReference)
.portForward(localPort, podPort, reuse, persist, externalAddress);
logger.addMessageGroup(constants.PORT_FORWARDING_MESSAGE_GROUP, 'Port forwarding enabled');
logger.addMessageGroupMessage(constants.PORT_FORWARDING_MESSAGE_GROUP, `${label} port forward enabled on ${externalAddress || constants.LOCAL_HOST}:${portForwardPortNumber}`);
if (component !== undefined) {
component.metadata.portForwardConfigs ||= [];
// Check if this exact podPort and localPort pair already exists
const existingConfig = component.metadata.portForwardConfigs.find((config) => config.podPort === podPort && config.localPort === portForwardPortNumber);
if (existingConfig) {
logger.info(`port forward config already exists: localPort=${portForwardPortNumber}, podPort=${podPort}`);
}
else {
logger.info(`add port localPort=${portForwardPortNumber}, podPort=${podPort}`);
// Save port forward config to component
component.metadata.portForwardConfigs.push({
podPort: podPort,
localPort: portForwardPortNumber,
});
}
}
return portForwardPortNumber;
}
/**
* Stops port forwarding for a component by removing the configuration and stopping the forward
* @param clusterReference The cluster reference
* @param podReference The pod reference
* @param podPort The port on the pod
* @param localPort The local port
* @param k8Client The Kubernetes client to use for stopping port forwarding
* @param logger Logger for messages
* @param componentType The component type
* @param label Label for the port forward
* @param nodeId Optional node ID for finding component when cluster reference is not available
*/
async stopPortForwards(clusterReference, podReference, podPort, localPort, k8Client, logger, componentType, label, nodeId) {
// Find component by cluster reference or nodeId
let component;
if (clusterReference) {
const schemeComponents = this.getComponentsByClusterReference(componentType, clusterReference);
component = schemeComponents[0];
}
else {
const componentId = Templates.renderComponentIdFromNodeId(nodeId);
component = this.getComponentById(componentType, componentId);
}
if (component === undefined || !component.metadata.portForwardConfigs) {
logger.showUser(`No port forward config found for ${label}`);
return;
}
// Find the matching port forward config
const configIndex = component.metadata.portForwardConfigs.findIndex((config) => config.podPort === podPort && config.localPort === localPort);
if (configIndex === -1) {
logger.showUser(`Port forward config not found for ${label} with podPort=${podPort}, localPort=${localPort}`);
return;
}
// Stop the port forward - use any pod reference since stopping should work regardless of pod
// eslint-disable-next-line unicorn/no-null
await k8Client.pods().readByReference(null).stopPortForward(localPort);
logger.addMessageGroup(constants.PORT_FORWARDING_MESSAGE_GROUP, 'Port forwarding stopped');
logger.addMessageGroupMessage(constants.PORT_FORWARDING_MESSAGE_GROUP, `${label} port forward stopped on localhost:${localPort}`);
// Remove the config from component metadata
component.metadata.portForwardConfigs.splice(configIndex, 1);
}
}
//# sourceMappingURL=components-data-wrapper.js.map