@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
410 lines (340 loc) • 15.7 kB
text/typescript
// 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 {type DeploymentPhase} from '../../../data/schema/model/remote/deployment-phase.js';
import {type ClusterReferenceName, type ComponentId, type PortForwardConfig} from '../../../types/index.js';
import {type ComponentsDataWrapperApi} from './api/components-data-wrapper-api.js';
import {type DeploymentStateSchema} from '../../../data/schema/model/remote/deployment-state-schema.js';
import {type ConsensusNodeStateSchema} from '../../../data/schema/model/remote/state/consensus-node-state-schema.js';
import {type ComponentIdsStructure} from '../../../data/schema/model/remote/interfaces/components-ids-structure.js';
import {type PodReference} from '../../../integration/kube/resources/pod/pod-reference.js';
import {type K8} from '../../../integration/kube/k8.js';
import {type SoloLogger} from '../../logging/solo-logger.js';
import * as constants from '../../constants.js';
import {Templates} from '../../templates.js';
export class ComponentsDataWrapper implements ComponentsDataWrapperApi {
public constructor(public state: DeploymentStateSchema) {}
public get componentIds(): ComponentIdsStructure {
return this.state.componentIds;
}
/* -------- Modifiers -------- */
/** Used to add new component to their respective group. */
public addNewComponent(
component: BaseStateSchema,
type: ComponentTypes,
isReplace?: boolean,
skipIncrement: boolean = false,
): void {
const componentId: 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: BaseStateSchema[]) => void = (components): void => {
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
public changeNodePhase(componentId: ComponentId, phase: DeploymentPhase): void {
if (!this.state.consensusNodes.some((component): boolean => +component.metadata.id === +componentId)) {
throw new SoloError(`Consensus node ${componentId} doesn't exist`);
}
const component: ConsensusNodeStateSchema = this.state.consensusNodes.find(
(component): boolean => +component.metadata.id === +componentId,
);
component.metadata.phase = phase;
}
public changeComponentPhase(componentId: ComponentId, type: ComponentTypes, phase: DeploymentPhase): void {
if (typeof componentId !== 'number') {
throw new SoloError(`Component id is required ${componentId}`);
}
const updateComponentCallback: (components: BaseStateSchema[]) => void = (components): void => {
const component: BaseStateSchema = components.find((component): boolean => 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. */
public removeComponent(componentId: ComponentId, type: ComponentTypes): void {
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: BaseStateSchema[]) => void = (components): void => {
const index: number = components.findIndex((component): boolean => 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 -------- */
public getComponent<T extends BaseStateSchema>(type: ComponentTypes, componentId: ComponentId): T {
let component: T;
const getComponentCallback: (components: BaseStateSchema[]) => void = (components): void => {
component = components.find((component): boolean => component.metadata.id === componentId) as T;
if (!component) {
throw new SoloError(`Component ${componentId} of type ${type} not found while attempting to read`);
}
};
this.applyCallbackToComponentGroup(type, getComponentCallback, componentId);
return component;
}
public getComponentByType<T extends BaseStateSchema>(type: ComponentTypes): T[] {
let components: T[] = [];
const getComponentsByTypeCallback: (comps: BaseStateSchema[]) => void = (comps): void => {
components = comps as T[];
};
this.applyCallbackToComponentGroup(type, getComponentsByTypeCallback);
return components;
}
public getComponentsByClusterReference<T extends BaseStateSchema>(
type: ComponentTypes,
clusterReference: ClusterReferenceName,
): T[] {
let filteredComponents: T[] = [];
const getComponentsByClusterReferenceCallback: (components: T[]) => void = (components): void => {
filteredComponents = components.filter((component): boolean => component.metadata.cluster === clusterReference);
};
this.applyCallbackToComponentGroup(type, getComponentsByClusterReferenceCallback);
return filteredComponents;
}
public getComponentById<T extends BaseStateSchema>(type: ComponentTypes, id: number): T {
let filteredComponent: T;
const getComponentByIdCallback: (components: T[]) => void = (components): void => {
filteredComponent = components.find((component): boolean => +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
*/
private applyCallbackToComponentGroup(
componentType: ComponentTypes,
callback: (components: BaseStateSchema[]) => void,
componentId?: ComponentId,
): void {
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 */
private checkComponentExists(components: BaseStateSchema[], newComponent: BaseStateSchema): boolean {
return components.some((component): boolean => component.metadata.id === newComponent.metadata.id);
}
public getNewComponentId(componentType: ComponentTypes): number {
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
*/
public async managePortForward(
clusterReference: ClusterReferenceName,
podReference: PodReference,
podPort: number,
localPort: number,
k8Client: K8,
logger: SoloLogger,
componentType: ComponentTypes,
label: string,
reuse: boolean = false,
nodeId?: number,
persist: boolean = false,
externalAddress?: string,
): Promise<number> {
// found component by cluster reference or nodeId
let component: BaseStateSchema;
if (clusterReference) {
const schemeComponents: BaseStateSchema[] = this.getComponentsByClusterReference<BaseStateSchema>(
componentType,
clusterReference,
);
component = schemeComponents[0];
} else {
const componentId: ComponentId = Templates.renderComponentIdFromNodeId(nodeId);
component = this.getComponentById<BaseStateSchema>(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): boolean => !(c.podPort === podPort && c.localPort === portForwardConfig.localPort),
);
break;
}
}
}
// Enable port forwarding
const portForwardPortNumber: number = 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: PortForwardConfig | undefined = component.metadata.portForwardConfigs.find(
(config): boolean => 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
*/
public async stopPortForwards(
clusterReference: ClusterReferenceName,
podReference: PodReference,
podPort: number,
localPort: number,
k8Client: K8,
logger: SoloLogger,
componentType: ComponentTypes,
label: string,
nodeId?: number,
): Promise<void> {
// Find component by cluster reference or nodeId
let component: BaseStateSchema;
if (clusterReference) {
const schemeComponents: BaseStateSchema[] = this.getComponentsByClusterReference<BaseStateSchema>(
componentType,
clusterReference,
);
component = schemeComponents[0];
} else {
const componentId: ComponentId = Templates.renderComponentIdFromNodeId(nodeId);
component = this.getComponentById<BaseStateSchema>(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: number = component.metadata.portForwardConfigs.findIndex(
(config): boolean => 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);
}
}