@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
318 lines (273 loc) • 12.4 kB
text/typescript
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {ComponentType, ConsensusNodeStates} from './enumerations.js';
import {SoloError} from '../../errors.js';
import {BaseComponent} from './components/base_component.js';
import {RelayComponent} from './components/relay_component.js';
import {HaProxyComponent} from './components/ha_proxy_component.js';
import {MirrorNodeComponent} from './components/mirror_node_component.js';
import {EnvoyProxyComponent} from './components/envoy_proxy_component.js';
import {ConsensusNodeComponent} from './components/consensus_node_component.js';
import {MirrorNodeExplorerComponent} from './components/mirror_node_explorer_component.js';
import {
type Component,
type ComponentsDataStructure,
type IConsensusNodeComponent,
type IRelayComponent,
type ComponentName,
type NamespaceNameAsString,
} from './types.js';
import {type ToObject, type Validate} from '../../../types/index.js';
/**
* Represent the components in the remote config and handles:
* - CRUD operations on the components.
* - Validation.
* - Conversion FROM and TO plain object.
*/
export class ComponentsDataWrapper implements Validate, ToObject<ComponentsDataStructure> {
/**
* @param relays - Relay record mapping service name to relay components
* @param haProxies - HA Proxies record mapping service name to ha proxies components
* @param mirrorNodes - Mirror Nodes record mapping service name to mirror nodes components
* @param envoyProxies - Envoy Proxies record mapping service name to envoy proxies components
* @param consensusNodes - Consensus Nodes record mapping service name to consensus nodes components
* @param mirrorNodeExplorers - Mirror Node Explorers record mapping service name to mirror node explorers components
*/
private constructor(
public readonly relays: Record<ComponentName, RelayComponent> = {},
public readonly haProxies: Record<ComponentName, HaProxyComponent> = {},
public readonly mirrorNodes: Record<ComponentName, MirrorNodeComponent> = {},
public readonly envoyProxies: Record<ComponentName, EnvoyProxyComponent> = {},
public readonly consensusNodes: Record<ComponentName, ConsensusNodeComponent> = {},
public readonly mirrorNodeExplorers: Record<ComponentName, MirrorNodeExplorerComponent> = {},
) {
this.validate();
}
/* -------- Modifiers -------- */
/** Used to add new component to their respective group. */
public add(serviceName: ComponentName, component: BaseComponent): void {
const self = this;
if (!serviceName || typeof serviceName !== 'string') {
throw new SoloError(`Service name is required ${serviceName}`);
}
if (!(component instanceof BaseComponent)) {
throw new SoloError('Component must be instance of BaseComponent', null, BaseComponent);
}
function addComponentCallback(components: Record<ComponentName, BaseComponent>): void {
if (self.exists(components, component)) {
throw new SoloError('Component exists', null, component.toObject());
}
components[serviceName] = component;
}
self.applyCallbackToComponentGroup(component.type, serviceName, addComponentCallback);
}
/** Used to edit an existing component from their respective group. */
public edit(serviceName: ComponentName, component: BaseComponent): void {
const self = this;
if (!serviceName || typeof serviceName !== 'string') {
throw new SoloError(`Service name is required ${serviceName}`);
}
if (!(component instanceof BaseComponent)) {
throw new SoloError('Component must be instance of BaseComponent', null, BaseComponent);
}
function editComponentCallback(components: Record<ComponentName, BaseComponent>): void {
if (!components[serviceName]) {
throw new SoloError(`Component doesn't exist, name: ${serviceName}`, null, {component});
}
components[serviceName] = component;
}
self.applyCallbackToComponentGroup(component.type, serviceName, editComponentCallback);
}
/** Used to remove specific component from their respective group. */
public remove(serviceName: ComponentName, type: ComponentType): void {
const self = this;
if (!serviceName || typeof serviceName !== 'string') {
throw new SoloError(`Service name is required ${serviceName}`);
}
if (!Object.values(ComponentType).includes(type)) {
throw new SoloError(`Invalid component type ${type}`);
}
function deleteComponentCallback(components: Record<ComponentName, BaseComponent>): void {
if (!components[serviceName]) {
throw new SoloError(`Component ${serviceName} of type ${type} not found while attempting to remove`);
}
delete components[serviceName];
}
self.applyCallbackToComponentGroup(type, serviceName, deleteComponentCallback);
}
/* -------- Utilities -------- */
public getComponent<T extends BaseComponent>(type: ComponentType, serviceName: ComponentName): T {
let component: T;
function getComponentCallback(components: Record<ComponentName, BaseComponent>): void {
if (!components[serviceName]) {
throw new SoloError(`Component ${serviceName} of type ${type} not found while attempting to read`);
}
component = components[serviceName] as T;
}
this.applyCallbackToComponentGroup(type, serviceName, getComponentCallback);
return component;
}
/**
* Method used to map the type to the specific component group
* and pass it to a callback to apply modifications
*/
private applyCallbackToComponentGroup(
type: ComponentType,
serviceName: ComponentName,
callback: (components: Record<ComponentName, BaseComponent>) => void,
): void {
switch (type) {
case ComponentType.Relay:
callback(this.relays);
break;
case ComponentType.HaProxy:
callback(this.haProxies);
break;
case ComponentType.MirrorNode:
callback(this.mirrorNodes);
break;
case ComponentType.EnvoyProxy:
callback(this.envoyProxies);
break;
case ComponentType.ConsensusNode:
callback(this.consensusNodes);
break;
case ComponentType.MirrorNodeExplorer:
callback(this.mirrorNodeExplorers);
break;
default:
throw new SoloError(`Unknown component type ${type}, service name: ${serviceName}`);
}
this.validate();
}
/**
* Handles creating instance of the class from plain object.
*
* @param components - component groups distinguished by their type.
*/
public static fromObject(components: ComponentsDataStructure): ComponentsDataWrapper {
const relays: Record<ComponentName, RelayComponent> = {};
const haProxies: Record<ComponentName, HaProxyComponent> = {};
const mirrorNodes: Record<ComponentName, MirrorNodeComponent> = {};
const envoyProxies: Record<ComponentName, EnvoyProxyComponent> = {};
const consensusNodes: Record<ComponentName, ConsensusNodeComponent> = {};
const mirrorNodeExplorers: Record<ComponentName, MirrorNodeExplorerComponent> = {};
Object.entries(components).forEach(
([type, components]: [ComponentType, Record<ComponentName, Component>]): void => {
switch (type) {
case ComponentType.Relay:
Object.entries(components).forEach(([name, component]: [ComponentName, IRelayComponent]): void => {
relays[name] = RelayComponent.fromObject(component);
});
break;
case ComponentType.HaProxy:
Object.entries(components).forEach(([name, component]: [ComponentName, Component]): void => {
haProxies[name] = HaProxyComponent.fromObject(component);
});
break;
case ComponentType.MirrorNode:
Object.entries(components).forEach(([name, component]: [ComponentName, Component]): void => {
mirrorNodes[name] = MirrorNodeComponent.fromObject(component);
});
break;
case ComponentType.EnvoyProxy:
Object.entries(components).forEach(([name, component]: [ComponentName, Component]): void => {
envoyProxies[name] = EnvoyProxyComponent.fromObject(component);
});
break;
case ComponentType.ConsensusNode:
Object.entries(components).forEach(([name, component]: [ComponentName, IConsensusNodeComponent]): void => {
consensusNodes[name] = ConsensusNodeComponent.fromObject(component);
});
break;
case ComponentType.MirrorNodeExplorer:
Object.entries(components).forEach(([name, component]: [ComponentName, Component]): void => {
mirrorNodeExplorers[name] = MirrorNodeExplorerComponent.fromObject(component);
});
break;
default:
throw new SoloError(`Unknown component type ${type}`);
}
},
);
return new ComponentsDataWrapper(relays, haProxies, mirrorNodes, envoyProxies, consensusNodes, mirrorNodeExplorers);
}
/** Used to create an empty instance used to keep the constructor private */
public static initializeEmpty(): ComponentsDataWrapper {
return new ComponentsDataWrapper();
}
public static initializeWithNodes(
nodeAliases: string[],
cluster: string,
namespace: NamespaceNameAsString,
): ComponentsDataWrapper {
const consensusNodeComponents: Record<ComponentName, ConsensusNodeComponent> = {};
nodeAliases.forEach((alias, index) => {
consensusNodeComponents[alias] = new ConsensusNodeComponent(
alias,
cluster,
namespace,
ConsensusNodeStates.REQUESTED,
index,
);
});
const componentDataWrapper = new ComponentsDataWrapper(
undefined,
undefined,
undefined,
undefined,
consensusNodeComponents,
undefined,
);
return componentDataWrapper;
}
/** checks if component exists in the respective group */
private exists(components: Record<ComponentName, BaseComponent>, newComponent: BaseComponent): boolean {
return Object.values(components).some(component => BaseComponent.compare(component, newComponent));
}
public validate(): void {
function testComponentsObject(components: Record<ComponentName, BaseComponent>, expectedInstance: any): void {
Object.entries(components).forEach(([name, component]: [ComponentName, BaseComponent]): void => {
if (!name || typeof name !== 'string') {
throw new SoloError(`Invalid component service name ${{[name]: component?.constructor?.name}}`);
}
if (!(component instanceof expectedInstance)) {
throw new SoloError(
`Invalid component type, service name: ${name}, ` +
`expected ${expectedInstance?.name}, actual: ${component?.constructor?.name}`,
null,
{component},
);
}
});
}
testComponentsObject(this.relays, RelayComponent);
testComponentsObject(this.haProxies, HaProxyComponent);
testComponentsObject(this.mirrorNodes, MirrorNodeComponent);
testComponentsObject(this.envoyProxies, EnvoyProxyComponent);
testComponentsObject(this.consensusNodes, ConsensusNodeComponent);
testComponentsObject(this.mirrorNodeExplorers, MirrorNodeExplorerComponent);
}
public toObject(): ComponentsDataStructure {
function transform(components: Record<ComponentName, BaseComponent>): Record<ComponentName, Component> {
const transformedComponents: Record<ComponentName, Component> = {};
Object.entries(components).forEach(([name, component]: [ComponentName, BaseComponent]): void => {
transformedComponents[name] = component.toObject() as Component;
});
return transformedComponents;
}
return {
[ComponentType.Relay]: transform(this.relays),
[ComponentType.HaProxy]: transform(this.haProxies),
[ComponentType.MirrorNode]: transform(this.mirrorNodes),
[ComponentType.EnvoyProxy]: transform(this.envoyProxies),
[ComponentType.ConsensusNode]: transform(this.consensusNodes),
[ComponentType.MirrorNodeExplorer]: transform(this.mirrorNodeExplorers),
};
}
public clone(): ComponentsDataWrapper {
const data = this.toObject();
return ComponentsDataWrapper.fromObject(data);
}
}