From e8133964267297b75fe6658bfa6d4c2110f0655b Mon Sep 17 00:00:00 2001 From: instamenta Date: Wed, 4 Dec 2024 13:53:47 +0200 Subject: [PATCH 1/5] added improved logic for adding the remote component, moved the context interfaces in seperate files and experimented a bit with new ways to write Listr2 related types Signed-off-by: instamenta --- src/commands/mirror_node.ts | 4 +- src/core/config/remote/remote_config_tasks.ts | 109 +++++++++++------- src/core/config/remote/types.ts | 33 +++++- src/types/index.ts | 7 +- 4 files changed, 106 insertions(+), 47 deletions(-) diff --git a/src/commands/mirror_node.ts b/src/commands/mirror_node.ts index a30801d44..590288c68 100644 --- a/src/commands/mirror_node.ts +++ b/src/commands/mirror_node.ts @@ -420,7 +420,7 @@ export class MirrorNodeCommand extends BaseCommand { ); }, }, - RemoteConfigTasks.addMirrorNodeAndMirrorNodeToExplorer.bind(this)(), + RemoteConfigTasks.addMirrorNodeComponents.bind(this)(), ], { concurrent: false, @@ -517,7 +517,7 @@ export class MirrorNodeCommand extends BaseCommand { }, skip: ctx => !ctx.config.isChartInstalled, }, - RemoteConfigTasks.removeMirrorNodeAndMirrorNodeToExplorer.bind(this)(), + RemoteConfigTasks.removeMirrorNodeComponents.bind(this)(), ], { concurrent: false, diff --git a/src/core/config/remote/remote_config_tasks.ts b/src/core/config/remote/remote_config_tasks.ts index 8f65905eb..7ad49de27 100644 --- a/src/core/config/remote/remote_config_tasks.ts +++ b/src/core/config/remote/remote_config_tasks.ts @@ -20,21 +20,31 @@ import { EnvoyProxyComponent, MirrorNodeComponent, ConsensusNodeComponent, + MirrorNodeExplorerComponent, } from './components/index.js'; import {ComponentType, ConsensusNodeStates} from './enumerations.js'; import chalk from 'chalk'; import {SoloError} from '../../errors.js'; -import type {Listr, ListrTask} from 'listr2'; -import type {NodeAlias, NodeAliases} from '../../../types/aliases.js'; +import type {Listr} from 'listr2'; +import type {NodeAlias} from '../../../types/aliases.js'; import type {BaseCommand} from '../../../commands/base.js'; import type {RelayCommand} from '../../../commands/relay.js'; import type {NetworkCommand} from '../../../commands/network.js'; import type {DeploymentCommand} from '../../../commands/deployment.js'; import type {MirrorNodeCommand} from '../../../commands/mirror_node.js'; import type {NodeCommandHandlers} from '../../../commands/node/handlers.js'; -import type {Optional} from '../../../types/index.js'; -import {ComponentsDataWrapper} from './components_data_wrapper.js'; +import type {EmptyContextConfig, Optional, SoloListrTask} from '../../../types/index.js'; +import type {ComponentsDataWrapper} from './components_data_wrapper.js'; +import type { + ValidateStatesObject, + AddRelayComponentContext, + AddNodesAndProxiesContext, + ChangeAllNodeStatesContext, + ValidateAllNodeStatesContext, + ValidateSingleNodeStateContext, + AddMirrorNodeComponentsContext, +} from './types.js'; /** * Static class that handles all tasks related to remote config used by other commands. @@ -47,19 +57,19 @@ export class RemoteConfigTasks { * * @param argv - used to update the last executed command and command history */ - public static loadRemoteConfig(this: BaseCommand, argv: any): ListrTask { + public static loadRemoteConfig(this: BaseCommand, argv: any): SoloListrTask { return this.remoteConfigManager.buildLoadTask(argv); } /** Creates remote config. */ - public static createRemoteConfig(this: DeploymentCommand): ListrTask { + public static createRemoteConfig(this: DeploymentCommand): SoloListrTask { return this.remoteConfigManager.buildCreateTask(); } /* ----------- Component Modifying ----------- */ /** Adds the relay component to remote config. */ - public static addRelayComponent(this: RelayCommand): ListrTask { + public static addRelayComponent(this: RelayCommand): SoloListrTask { return { title: 'Add relay component in remote config', skip: (): boolean => !this.remoteConfigManager.isLoaded(), @@ -70,14 +80,16 @@ export class RemoteConfigTasks { } = ctx; const cluster = this.remoteConfigManager.currentCluster; - remoteConfig.components.add('relay', new RelayComponent('relay', cluster, namespace, nodeAliases)); + const component = new RelayComponent('relay', cluster, namespace, nodeAliases); + + remoteConfig.components.add('relay', component); }); }, }; } /** Remove the relay component from remote config. */ - public static removeRelayComponent(this: RelayCommand): ListrTask { + public static removeRelayComponent(this: RelayCommand): SoloListrTask { return { title: 'Remove relay component from remote config', skip: (): boolean => !this.remoteConfigManager.isLoaded(), @@ -90,30 +102,42 @@ export class RemoteConfigTasks { } /** Adds the mirror node and mirror node explorer components to remote config. */ - public static addMirrorNodeAndMirrorNodeToExplorer(this: MirrorNodeCommand): ListrTask { + public static addMirrorNodeComponents(this: MirrorNodeCommand): SoloListrTask { return { title: 'Add mirror node and mirror node explorer to remote config', skip: (): boolean => !this.remoteConfigManager.isLoaded(), task: async (ctx): Promise => { await this.remoteConfigManager.modify(async remoteConfig => { const { - config: {namespace}, + config: {namespace, deployHederaExplorer}, } = ctx; const cluster = this.remoteConfigManager.currentCluster; - remoteConfig.components.add('mirrorNode', new MirrorNodeComponent('mirrorNode', cluster, namespace)); + try { + const component = new MirrorNodeComponent('mirrorNode', cluster, namespace); + + remoteConfig.components.add('mirrorNode', component); + } catch (e) { + throw new SoloError('Mirror node component already exists', e); + } + + // Add a mirror node explorer component to remote config only if the flag is enabled + if (!deployHederaExplorer) return; + + try { + const component = new MirrorNodeExplorerComponent('mirrorNodeExplorer', cluster, namespace); - remoteConfig.components.add( - 'mirrorNodeExplorer', - new MirrorNodeComponent('mirrorNodeExplorer', cluster, namespace), - ); + remoteConfig.components.add('mirrorNodeExplorer', component); + } catch (e) { + throw new SoloError('Mirror node explorer component already exists', e); + } }); }, }; } /** Removes the mirror node and mirror node explorer components from remote config. */ - public static removeMirrorNodeAndMirrorNodeToExplorer(this: MirrorNodeCommand): ListrTask { + public static removeMirrorNodeComponents(this: MirrorNodeCommand): SoloListrTask { return { title: 'Remove mirror node and mirror node explorer from remote config', skip: (): boolean => !this.remoteConfigManager.isLoaded(), @@ -121,14 +145,19 @@ export class RemoteConfigTasks { await this.remoteConfigManager.modify(async remoteConfig => { remoteConfig.components.remove('mirrorNode', ComponentType.MirrorNode); - remoteConfig.components.remove('mirrorNodeExplorer', ComponentType.MirrorNode); + try { + remoteConfig.components.remove('mirrorNodeExplorer', ComponentType.MirrorNode); + } catch { + // When the mirror node explorer component is not deployed, + // error is thrown since is not found, in this case ignore it + } }); }, }; } /** Adds the consensus node, envoy and haproxy components to remote config. */ - public static addNodesAndProxies(this: NetworkCommand): ListrTask { + public static addNodesAndProxies(this: NetworkCommand): SoloListrTask { return { title: 'Add node and proxies to remote config', skip: (): boolean => !this.remoteConfigManager.isLoaded(), @@ -161,7 +190,7 @@ export class RemoteConfigTasks { } /** Removes the consensus node, envoy and haproxy components from remote config. */ - public static removeNodeAndProxies(this: NodeCommandHandlers): ListrTask { + public static removeNodeAndProxies(this: NodeCommandHandlers): SoloListrTask { return { skip: (): boolean => !this.remoteConfigManager.isLoaded(), title: 'Remove node and proxies from remote config', @@ -180,23 +209,25 @@ export class RemoteConfigTasks { * * @param state - to which to change the consensus node component */ - public static changeAllNodeStates(this: NodeCommandHandlers, state: ConsensusNodeStates): ListrTask { - interface Context { - config: {namespace: string; nodeAliases: NodeAliases}; - } - + public static changeAllNodeStates( + this: NodeCommandHandlers, + state: ConsensusNodeStates, + ): SoloListrTask { return { title: `Change node state to ${state} in remote config`, skip: (): boolean => !this.remoteConfigManager.isLoaded(), - task: async (ctx: Context): Promise => { + task: async (ctx): Promise => { await this.remoteConfigManager.modify(async remoteConfig => { const { config: {namespace, nodeAliases}, } = ctx; + const cluster = this.remoteConfigManager.currentCluster; for (const nodeAlias of nodeAliases) { - remoteConfig.components.edit(nodeAlias, new ConsensusNodeComponent(nodeAlias, cluster, namespace, state)); + const component = new ConsensusNodeComponent(nodeAlias, cluster, namespace, state); + + remoteConfig.components.edit(nodeAlias, component); } }); }, @@ -211,21 +242,17 @@ export class RemoteConfigTasks { */ public static validateAllNodeStates( this: NodeCommandHandlers, - {acceptedStates, excludedStates}: {acceptedStates?: ConsensusNodeStates[]; excludedStates?: ConsensusNodeStates[]}, - ): ListrTask { - interface Context { - config: {namespace: string; nodeAliases: NodeAliases}; - } - + {acceptedStates, excludedStates}: ValidateStatesObject, + ): SoloListrTask { return { title: 'Validate nodes states', skip: (): boolean => !this.remoteConfigManager.isLoaded(), - task: (ctx: Context, task): Listr => { + task: (ctx, task): Listr => { const nodeAliases = ctx.config.nodeAliases; const components = this.remoteConfigManager.components; - const subTasks: ListrTask[] = nodeAliases.map(nodeAlias => ({ + const subTasks: SoloListrTask[] = nodeAliases.map(nodeAlias => ({ title: `Validating state for node ${nodeAlias}`, task: (_, task): void => { const state = RemoteConfigTasks.validateNodeState(nodeAlias, components, acceptedStates, excludedStates); @@ -250,16 +277,12 @@ export class RemoteConfigTasks { */ public static validateSingleNodeState( this: NodeCommandHandlers, - {acceptedStates, excludedStates}: {acceptedStates?: ConsensusNodeStates[]; excludedStates?: ConsensusNodeStates[]}, - ): ListrTask { - interface Context { - config: {namespace: string; nodeAlias: NodeAlias}; - } - + {acceptedStates, excludedStates}: ValidateStatesObject, + ): SoloListrTask { return { title: 'Validate nodes state', skip: (): boolean => !this.remoteConfigManager.isLoaded(), - task: (ctx: Context, task): void => { + task: (ctx, task): void => { const nodeAlias = ctx.config.nodeAlias; task.title += ` ${nodeAlias}`; @@ -289,7 +312,7 @@ export class RemoteConfigTasks { try { nodeComponent = components.getComponent(ComponentType.ConsensusNode, nodeAlias); } catch (e) { - throw new SoloError(`${nodeAlias} not found in remote config`); + throw new SoloError(`${nodeAlias} not found in remote config`, e); } if (acceptedStates && !acceptedStates.includes(nodeComponent.state)) { diff --git a/src/core/config/remote/types.ts b/src/core/config/remote/types.ts index 3756b4174..e0b82d39d 100644 --- a/src/core/config/remote/types.ts +++ b/src/core/config/remote/types.ts @@ -14,7 +14,7 @@ * limitations under the License. * */ -import type {NodeAliases} from '../../../types/aliases.js'; +import type {NodeAlias, NodeAliases} from '../../../types/aliases.js'; import type {Migration} from './migration.js'; import type {ComponentsDataWrapper} from './components_data_wrapper.js'; import type {RemoteConfigMetadata} from './metadata.js'; @@ -72,3 +72,34 @@ export interface RemoteConfigDataStructure { commandHistory: string[]; lastExecutedCommand: string; } + +export interface ValidateStatesObject { + acceptedStates?: ConsensusNodeStates[]; + excludedStates?: ConsensusNodeStates[]; +} + +// ---------- Component Modifying Tasks Contexts ---------- // + +export interface AddRelayComponentContext { + config: {namespace: Namespace; nodeAliases: NodeAliases}; +} + +export interface AddMirrorNodeComponentsContext { + config: {namespace: Namespace; deployHederaExplorer: boolean}; +} + +export interface AddNodesAndProxiesContext { + config: {namespace: Namespace; nodeAliases: NodeAliases}; +} + +export interface ChangeAllNodeStatesContext { + config: {namespace: Namespace; nodeAliases: NodeAliases}; +} + +export interface ValidateAllNodeStatesContext { + config: {namespace: string; nodeAliases: NodeAliases}; +} + +export interface ValidateSingleNodeStateContext { + config: {namespace: string; nodeAlias: NodeAlias}; +} diff --git a/src/types/index.ts b/src/types/index.ts index 215bb1963..1a5076b22 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,8 +35,9 @@ import type { RemoteConfigManager, LocalConfig, } from '../core/index.js'; -import type {Cluster, Context} from '../core/config/remote/types.js'; +import {Cluster, Context} from '../core/config/remote/types.js'; import {type BaseCommand} from '../commands/base.js'; +import type {ListrTask} from 'listr2'; export interface NodeKeyObject { privateKey: crypto.webcrypto.CryptoKey; @@ -130,3 +131,7 @@ export interface ToObject { */ toObject(): T; } + +export type SoloListrTask = ListrTask; + +export type EmptyContextConfig = object; From 9de7dc91f548a4a51362d39b806b122ba70359b2 Mon Sep 17 00:00:00 2001 From: instamenta Date: Wed, 4 Dec 2024 17:47:50 +0200 Subject: [PATCH 2/5] implemented validation, proper remote config components tracking and template renders and labels selectors added for components tracked by remote config Signed-off-by: instamenta --- .../config/remote/components_data_wrapper.ts | 12 +- .../config/remote/remote_config_manager.ts | 11 +- src/core/config/remote/remote_config_tasks.ts | 15 +-- .../config/remote/remote_config_validator.ts | 124 ++++++++++++++++++ src/core/constants.ts | 7 + src/core/templates.ts | 8 ++ 6 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 src/core/config/remote/remote_config_validator.ts diff --git a/src/core/config/remote/components_data_wrapper.ts b/src/core/config/remote/components_data_wrapper.ts index 36d3a4776..1b47081ab 100644 --- a/src/core/config/remote/components_data_wrapper.ts +++ b/src/core/config/remote/components_data_wrapper.ts @@ -50,12 +50,12 @@ export class ComponentsDataWrapper implements Validate, ToObject = {}, - private readonly haProxies: Record = {}, - private readonly mirrorNodes: Record = {}, - private readonly envoyProxies: Record = {}, - private readonly consensusNodes: Record = {}, - private readonly mirrorNodeExplorers: Record = {}, + public readonly relays: Record = {}, + public readonly haProxies: Record = {}, + public readonly mirrorNodes: Record = {}, + public readonly envoyProxies: Record = {}, + public readonly consensusNodes: Record = {}, + public readonly mirrorNodeExplorers: Record = {}, ) { this.validate(); } diff --git a/src/core/config/remote/remote_config_manager.ts b/src/core/config/remote/remote_config_manager.ts index 1d574c832..8cb2a0742 100644 --- a/src/core/config/remote/remote_config_manager.ts +++ b/src/core/config/remote/remote_config_manager.ts @@ -22,14 +22,14 @@ import {RemoteConfigMetadata} from './metadata.js'; import {flags} from '../../../commands/index.js'; import * as yaml from 'yaml'; import {ComponentsDataWrapper} from './components_data_wrapper.js'; +import {RemoteConfigValidator} from './remote_config_validator.js'; import type {K8} from '../../k8.js'; import type {Cluster, Namespace} from './types.js'; import type {SoloLogger} from '../../logging.js'; -import type {ListrTask} from 'listr2'; import type {ConfigManager} from '../../config_manager.js'; import type {LocalConfig} from '../local_config.js'; import type {DeploymentStructure} from '../local_config_data.js'; -import type {ContextClusterStructure, Optional} from '../../../types/index.js'; +import type {ContextClusterStructure, EmptyContextConfig, Optional, SoloListrTask} from '../../../types/index.js'; import type * as k8s from '@kubernetes/client-node'; interface ListrContext { @@ -137,6 +137,7 @@ export class RemoteConfigManager { if (!configMap) return false; this.remoteConfig = RemoteConfigDataWrapper.fromConfigmap(configMap); + return true; } @@ -149,7 +150,7 @@ export class RemoteConfigManager { * @param argv - arguments containing command input for historical reference. * @returns a Listr task which loads the remote configuration. */ - public buildLoadTask(argv: {_: string[]}): ListrTask { + public buildLoadTask(argv: {_: string[]}): SoloListrTask { const self = this; return { @@ -170,6 +171,8 @@ export class RemoteConfigManager { // throw new SoloError('Failed to load remote config') } + await RemoteConfigValidator.validateComponents(self.remoteConfig.components, self.k8); + const currentCommand = argv._.join(' '); self.remoteConfig!.addCommandToHistory(currentCommand); @@ -184,7 +187,7 @@ export class RemoteConfigManager { * * @returns a Listr task which creates the remote configuration. */ - public buildCreateTask(): ListrTask { + public buildCreateTask(): SoloListrTask { const self = this; return { diff --git a/src/core/config/remote/remote_config_tasks.ts b/src/core/config/remote/remote_config_tasks.ts index 7ad49de27..962858d35 100644 --- a/src/core/config/remote/remote_config_tasks.ts +++ b/src/core/config/remote/remote_config_tasks.ts @@ -45,6 +45,7 @@ import type { ValidateSingleNodeStateContext, AddMirrorNodeComponentsContext, } from './types.js'; +import {Templates} from '../../templates.js'; /** * Static class that handles all tasks related to remote config used by other commands. @@ -174,15 +175,13 @@ export class RemoteConfigTasks { new ConsensusNodeComponent(nodeAlias, cluster, namespace, ConsensusNodeStates.INITIALIZED), ); - remoteConfig.components.add( - `envoy-${nodeAlias}`, - new EnvoyProxyComponent(`envoy-${nodeAlias}`, cluster, namespace), - ); + const envoyProxyName = Templates.renderEnvoyProxyName(nodeAlias); - remoteConfig.components.add( - `haproxy-${nodeAlias}`, - new HaProxyComponent(`haproxy-${nodeAlias}`, cluster, namespace), - ); + remoteConfig.components.add(envoyProxyName, new EnvoyProxyComponent(envoyProxyName, cluster, namespace)); + + const haProxyName = Templates.renderHaProxyName(nodeAlias); + + remoteConfig.components.add(haProxyName, new HaProxyComponent(haProxyName, cluster, namespace)); } }); }, diff --git a/src/core/config/remote/remote_config_validator.ts b/src/core/config/remote/remote_config_validator.ts new file mode 100644 index 000000000..e4e129851 --- /dev/null +++ b/src/core/config/remote/remote_config_validator.ts @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import * as constants from '../../constants.js'; + +import type {K8} from '../../k8.js'; +import type {ComponentsDataWrapper} from './components_data_wrapper.js'; + +export class RemoteConfigValidator { + public static async validateComponents(components: ComponentsDataWrapper, k8: K8): Promise { + await Promise.all([ + RemoteConfigValidator.validateRelays(components, k8), + RemoteConfigValidator.validateHaProxies(components, k8), + RemoteConfigValidator.validateMirrorNodes(components, k8), + RemoteConfigValidator.validateEnvoyProxies(components, k8), + RemoteConfigValidator.validateConsensusNodes(components, k8), + RemoteConfigValidator.validateMirrorNodeExplorers(components, k8), + ]); + } + + private static async validateRelays(components: ComponentsDataWrapper, k8: K8): Promise { + await Promise.all( + Object.values(components.relays).map(async component => { + try { + await k8.getPodsByLabel([constants.SOLO_RELAY_LABEL]); + } catch (e) { + throw new Error( + `Relay in remote config with name ${component.name} was not found in namespace ${component.namespace}`, + e, + ); + } + }), + ); + } + + private static async validateHaProxies(components: ComponentsDataWrapper, k8: K8): Promise { + await Promise.all( + Object.values(components.haProxies).map(async component => { + try { + await k8.getPodByName(component.name); + } catch (e) { + throw new Error( + `HaProxy in remote config with name ${component.name} was not found in namespace ${component.namespace}`, + e, + ); + } + }), + ); + } + + private static async validateMirrorNodes(components: ComponentsDataWrapper, k8: K8): Promise { + await Promise.all( + Object.values(components.mirrorNodes).map(async component => { + try { + await k8.getPodsByLabel(constants.SOLO_HEDERA_MIRROR_IMPORTER); + } catch (e) { + throw new Error( + `Mirror node in remote config with name ${component.name} was not found in namespace ${component.namespace}`, + e, + ); + } + }), + ); + } + + private static async validateEnvoyProxies(components: ComponentsDataWrapper, k8: K8): Promise { + await Promise.all( + Object.values(components.envoyProxies).map(async component => { + try { + await k8.getPodByName(component.name); + } catch (e) { + throw new Error( + `Envoy proxy in remote config with name ${component.name} was not found in namespace ${component.namespace}`, + e, + ); + } + }), + ); + } + + private static async validateConsensusNodes(components: ComponentsDataWrapper, k8: K8): Promise { + await Promise.all( + Object.values(components.consensusNodes).map(async component => { + try { + await k8.getPodByName(component.name); + } catch (e) { + throw new Error( + `Consensus node in remote config with name ${component.name} was not found in namespace ${component.namespace}`, + e, + ); + } + }), + ); + } + + private static async validateMirrorNodeExplorers(components: ComponentsDataWrapper, k8: K8): Promise { + await Promise.all( + Object.values(components.mirrorNodeExplorers).map(async component => { + try { + await k8.getPodsByLabel([constants.SOLO_HEDERA_EXPLORER_LABEL]); + } catch (e) { + throw new Error( + `Mirror node explorer in remote config with name ${component.name}` + + ` was not found in namespace ${component.namespace}`, + e, + ); + } + }), + ); + } +} diff --git a/src/core/constants.ts b/src/core/constants.ts index 2550090fe..7e25ab562 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -64,6 +64,13 @@ export const MIRROR_NODE_CHART = 'hedera-mirror'; export const MIRROR_NODE_RELEASE_NAME = 'mirror'; export const HEDERA_EXPLORER_CHART_UTL = 'oci://ghcr.io/hashgraph/hedera-mirror-node-explorer/hedera-explorer'; export const HEDERA_EXPLORER_CHART = 'hedera-explorer'; +export const SOLO_RELAY_LABEL = 'app=hedera-json-rpc-relay'; +export const SOLO_HEDERA_EXPLORER_LABEL = 'app.kubernetes.io/name=hedera-explorer'; + +export const SOLO_HEDERA_MIRROR_IMPORTER = [ + 'app.kubernetes.io/component=importer', + 'app.kubernetes.io/instance=mirror', +]; export const DEFAULT_CHART_REPO: Map = new Map() .set(JSON_RPC_RELAY_CHART, JSON_RPC_RELAY_CHART_URL) diff --git a/src/core/templates.ts b/src/core/templates.ts index 855c93d5a..c12f523aa 100644 --- a/src/core/templates.ts +++ b/src/core/templates.ts @@ -255,4 +255,12 @@ export class Templates { static parseClusterAliases(clusters: string) { return clusters ? clusters.split(',') : []; } + + public static renderEnvoyProxyName(nodeAlias: NodeAlias): string { + return `envoy-proxy-${nodeAlias}`; + } + + public static renderHaProxyName(nodeAlias: NodeAlias): string { + return `haproxy-${nodeAlias}`; + } } From 99b0e17e986f3bfcd38cb28b8d96de779d9f899e Mon Sep 17 00:00:00 2001 From: instamenta Date: Thu, 5 Dec 2024 15:50:09 +0200 Subject: [PATCH 3/5] added tests for validator class Signed-off-by: instamenta --- .../config/remote/remote_config_validator.ts | 184 ++++++++------- .../core/remote_config_validator.test.ts | 221 ++++++++++++++++++ 2 files changed, 321 insertions(+), 84 deletions(-) create mode 100644 test/e2e/integration/core/remote_config_validator.test.ts diff --git a/src/core/config/remote/remote_config_validator.ts b/src/core/config/remote/remote_config_validator.ts index e4e129851..47c6f78da 100644 --- a/src/core/config/remote/remote_config_validator.ts +++ b/src/core/config/remote/remote_config_validator.ts @@ -15,110 +15,126 @@ * */ import * as constants from '../../constants.js'; +import {SoloError} from '../../errors.js'; import type {K8} from '../../k8.js'; import type {ComponentsDataWrapper} from './components_data_wrapper.js'; +import type {BaseComponent} from './components/index.js'; +/** + * Static class is used to validate that components in the remote config + * are present in the kubernetes cluster, and throw errors if there is mismatch. + */ export class RemoteConfigValidator { + /** + * Gathers together and handles validation of all components. + * + * @param components - components which to validate. + * @param k8 - to validate the elements. + * TODO: Make compatible with multi-cluster K8 implementation + */ public static async validateComponents(components: ComponentsDataWrapper, k8: K8): Promise { await Promise.all([ - RemoteConfigValidator.validateRelays(components, k8), - RemoteConfigValidator.validateHaProxies(components, k8), - RemoteConfigValidator.validateMirrorNodes(components, k8), - RemoteConfigValidator.validateEnvoyProxies(components, k8), - RemoteConfigValidator.validateConsensusNodes(components, k8), - RemoteConfigValidator.validateMirrorNodeExplorers(components, k8), + ...RemoteConfigValidator.validateRelays(components, k8), + ...RemoteConfigValidator.validateHaProxies(components, k8), + ...RemoteConfigValidator.validateMirrorNodes(components, k8), + ...RemoteConfigValidator.validateEnvoyProxies(components, k8), + ...RemoteConfigValidator.validateConsensusNodes(components, k8), + ...RemoteConfigValidator.validateMirrorNodeExplorers(components, k8), ]); } - private static async validateRelays(components: ComponentsDataWrapper, k8: K8): Promise { - await Promise.all( - Object.values(components.relays).map(async component => { - try { - await k8.getPodsByLabel([constants.SOLO_RELAY_LABEL]); - } catch (e) { - throw new Error( - `Relay in remote config with name ${component.name} was not found in namespace ${component.namespace}`, - e, - ); - } - }), - ); + private static validateRelays(components: ComponentsDataWrapper, k8: K8): Promise[] { + return Object.values(components.relays).map(async component => { + try { + const pods = await k8.getPodsByLabel([constants.SOLO_RELAY_LABEL]); + + // to return the generic error message + if (!pods.length) throw new Error('Pod not found'); + } catch (e) { + RemoteConfigValidator.throwValidationError('Relay', component, e); + } + }); } - private static async validateHaProxies(components: ComponentsDataWrapper, k8: K8): Promise { - await Promise.all( - Object.values(components.haProxies).map(async component => { - try { - await k8.getPodByName(component.name); - } catch (e) { - throw new Error( - `HaProxy in remote config with name ${component.name} was not found in namespace ${component.namespace}`, - e, - ); - } - }), - ); + private static validateHaProxies(components: ComponentsDataWrapper, k8: K8): Promise[] { + return Object.values(components.haProxies).map(async component => { + try { + const pod = await k8.getPodByName(component.name); + + // to return the generic error message + if (!pod) throw new Error('Pod not found'); + } catch (e) { + RemoteConfigValidator.throwValidationError('HaProxy', component, e); + } + }); } - private static async validateMirrorNodes(components: ComponentsDataWrapper, k8: K8): Promise { - await Promise.all( - Object.values(components.mirrorNodes).map(async component => { - try { - await k8.getPodsByLabel(constants.SOLO_HEDERA_MIRROR_IMPORTER); - } catch (e) { - throw new Error( - `Mirror node in remote config with name ${component.name} was not found in namespace ${component.namespace}`, - e, - ); - } - }), - ); + private static validateMirrorNodes(components: ComponentsDataWrapper, k8: K8): Promise[] { + return Object.values(components.mirrorNodes).map(async component => { + try { + const pods = await k8.getPodsByLabel(constants.SOLO_HEDERA_MIRROR_IMPORTER); + + // to return the generic error message + if (!pods.length) throw new Error('Pod not found'); + } catch (e) { + RemoteConfigValidator.throwValidationError('Mirror node', component, e); + } + }); } - private static async validateEnvoyProxies(components: ComponentsDataWrapper, k8: K8): Promise { - await Promise.all( - Object.values(components.envoyProxies).map(async component => { - try { - await k8.getPodByName(component.name); - } catch (e) { - throw new Error( - `Envoy proxy in remote config with name ${component.name} was not found in namespace ${component.namespace}`, - e, - ); - } - }), - ); + private static validateEnvoyProxies(components: ComponentsDataWrapper, k8: K8): Promise[] { + return Object.values(components.envoyProxies).map(async component => { + try { + const pod = await k8.getPodByName(component.name); + + // to return the generic error message + if (!pod) throw new Error('Pod not found'); + } catch (e) { + RemoteConfigValidator.throwValidationError('Envoy proxy', component, e); + } + }); } - private static async validateConsensusNodes(components: ComponentsDataWrapper, k8: K8): Promise { - await Promise.all( - Object.values(components.consensusNodes).map(async component => { - try { - await k8.getPodByName(component.name); - } catch (e) { - throw new Error( - `Consensus node in remote config with name ${component.name} was not found in namespace ${component.namespace}`, - e, - ); - } - }), - ); + private static validateConsensusNodes(components: ComponentsDataWrapper, k8: K8): Promise[] { + return Object.values(components.consensusNodes).map(async component => { + try { + const pod = await k8.getPodByName(component.name); + + // to return the generic error message + if (!pod) throw new Error('Pod not found'); + } catch (e) { + RemoteConfigValidator.throwValidationError('Consensus node', component, e); + } + }); + } + + private static validateMirrorNodeExplorers(components: ComponentsDataWrapper, k8: K8): Promise[] { + return Object.values(components.mirrorNodeExplorers).map(async component => { + try { + const pods = await k8.getPodsByLabel([constants.SOLO_HEDERA_EXPLORER_LABEL]); + + // to return the generic error message + if (!pods.length) throw new Error('Pod not found'); + } catch (e) { + RemoteConfigValidator.throwValidationError('Mirror node explorer', component, e); + } + }); } - private static async validateMirrorNodeExplorers(components: ComponentsDataWrapper, k8: K8): Promise { - await Promise.all( - Object.values(components.mirrorNodeExplorers).map(async component => { - try { - await k8.getPodsByLabel([constants.SOLO_HEDERA_EXPLORER_LABEL]); - } catch (e) { - throw new Error( - `Mirror node explorer in remote config with name ${component.name}` + - ` was not found in namespace ${component.namespace}`, - e, - ); - } - }), + /** + * Generic handler that throws errors. + * + * @param type - name to display in error message + * @param component - component which is not found in the cluster + * @param e - original error for the kube client + */ + private static throwValidationError(type: string, component: BaseComponent, e: Error | unknown): never { + throw new SoloError( + `${type} in remote config with name ${component.name} ` + + `was not found in namespace: ${component.namespace}, cluster: ${component.cluster}`, + e, + {component: component.toObject()}, ); } } diff --git a/test/e2e/integration/core/remote_config_validator.test.ts b/test/e2e/integration/core/remote_config_validator.test.ts new file mode 100644 index 000000000..4282d66ee --- /dev/null +++ b/test/e2e/integration/core/remote_config_validator.test.ts @@ -0,0 +1,221 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import {it, describe} from 'mocha'; +import {expect} from 'chai'; + +import * as constants from '../../../../src/core/constants.js'; +import {ConfigManager, K8, Templates} from '../../../../src/core/index.js'; +import {testLogger} from '../../../test_util.js'; +import {flags} from '../../../../src/commands/index.js'; +import {V1Container, V1ExecAction, V1ObjectMeta, V1Pod, V1PodSpec, V1Probe} from '@kubernetes/client-node'; +import {RemoteConfigValidator} from '../../../../src/core/config/remote/remote_config_validator.js'; +import {ConsensusNodeStates} from '../../../../src/core/config/remote/enumerations.js'; +import { + ConsensusNodeComponent, + EnvoyProxyComponent, + HaProxyComponent, + MirrorNodeComponent, + MirrorNodeExplorerComponent, + RelayComponent, +} from '../../../../src/core/config/remote/components/index.js'; +import {ComponentsDataWrapper} from '../../../../src/core/config/remote/components_data_wrapper.js'; +import {SoloError} from '../../../../src/core/errors.js'; + +import type {NodeAlias, NodeAliases} from '../../../../src/types/aliases.js'; + +describe('RemoteConfigValidator', () => { + const namespace = 'remote-config-validator'; + + const configManager = new ConfigManager(testLogger); + configManager.update({[flags.namespace.name]: namespace}); + const k8 = new K8(configManager, testLogger); + + before(async () => { + await k8.createNamespace(namespace); + }); + + after(async () => { + await k8.deleteNamespace(namespace); + }); + + const cluster = 'cluster'; + const state = ConsensusNodeStates.STARTED; + + const nodeAlias = 'node1' as NodeAlias; + const haProxyName = Templates.renderHaProxyName(nodeAlias); + const envoyProxyName = Templates.renderEnvoyProxyName(nodeAlias); + const relayName = 'relay'; + const mirrorNodeName = 'mirror-node'; + const mirrorNodeExplorerName = 'mirror-node-explorer'; + + const consensusNodeAliases = [nodeAlias] as NodeAliases; + + // @ts-ignore + const components = new ComponentsDataWrapper( + {[relayName]: new RelayComponent(relayName, cluster, namespace, consensusNodeAliases)}, + {[haProxyName]: new HaProxyComponent(haProxyName, cluster, namespace)}, + {[mirrorNodeName]: new MirrorNodeComponent(mirrorNodeName, cluster, namespace)}, + {[envoyProxyName]: new EnvoyProxyComponent(envoyProxyName, cluster, namespace)}, + {[nodeAlias]: new ConsensusNodeComponent(nodeAlias, cluster, namespace, state)}, + {[mirrorNodeExplorerName]: new MirrorNodeExplorerComponent(mirrorNodeExplorerName, cluster, namespace)}, + ); + + async function createPod(name: string, labels: Record) { + const v1Pod = new V1Pod(); + const v1Metadata = new V1ObjectMeta(); + v1Metadata.name = name; + v1Metadata.namespace = namespace; + v1Metadata.labels = labels; + v1Pod.metadata = v1Metadata; + const v1Container = new V1Container(); + v1Container.name = name; + v1Container.image = 'alpine:latest'; + v1Container.command = ['/bin/sh', '-c', 'apk update && apk upgrade && apk add --update bash && sleep 7200']; + const v1Probe = new V1Probe(); + const v1ExecAction = new V1ExecAction(); + v1ExecAction.command = ['bash', '-c', 'exit 0']; + v1Probe.exec = v1ExecAction; + v1Container.startupProbe = v1Probe; + const v1Spec = new V1PodSpec(); + v1Spec.containers = [v1Container]; + v1Pod.spec = v1Spec; + try { + await k8.kubeClient.createNamespacedPod(namespace, v1Pod); + } catch (e) { + console.error(e); + throw new Error('Error creating pod'); + } + } + + describe('Relays validation', () => { + it('should fail if component is not present', async () => { + try { + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateRelays(components, k8)); + throw new Error(); + } catch (e) { + expect(e).to.be.instanceOf(SoloError); + } + }); + + it('should succeed if component is present', async () => { + const [key, value] = constants.SOLO_RELAY_LABEL.split('='); + await createPod(relayName, {[key]: value}); + + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateRelays(components, k8)); + }); + }); + + describe('HaProxies validation', () => { + it('should fail if component is not present', async () => { + try { + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateHaProxies(components, k8)); + throw new Error(); + } catch (e) { + expect(e).to.be.instanceOf(SoloError); + } + }); + + it('should succeed if component is present', async () => { + await createPod(haProxyName, {}); + + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateHaProxies(components, k8)); + }); + }); + + describe('Mirror Node Components validation', () => { + it('should fail if component is not present', async () => { + try { + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateMirrorNodes(components, k8)); + throw new Error(); + } catch (e) { + expect(e).to.be.instanceOf(SoloError); + } + }); + + it('should succeed if component is present', async () => { + const [key1, value1] = constants.SOLO_HEDERA_MIRROR_IMPORTER[0].split('='); + const [key2, value2] = constants.SOLO_HEDERA_MIRROR_IMPORTER[1].split('='); + await createPod(mirrorNodeName, {[key1]: value1, [key2]: value2}); + + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateMirrorNodes(components, k8)); + }); + }); + + describe('Envoy Proxies validation', () => { + it('should fail if component is not present', async () => { + try { + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateEnvoyProxies(components, k8)); + throw new Error(); + } catch (e) { + expect(e).to.be.instanceOf(SoloError); + } + }); + + it('should succeed if component is present', async () => { + await createPod(envoyProxyName, {}); + + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateEnvoyProxies(components, k8)); + }); + }); + + describe('Consensus Nodes validation', () => { + it('should fail if component is not present', async () => { + try { + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateConsensusNodes(components, k8)); + throw new Error(); + } catch (e) { + expect(e).to.be.instanceOf(SoloError); + } + }); + + it('should succeed if component is present', async () => { + await createPod(nodeAlias, {}); + + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateConsensusNodes(components, k8)); + }); + }); + + describe('Mirror Node Explorers validation', () => { + it('should fail if component is not present', async () => { + try { + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateMirrorNodeExplorers(components, k8)); + throw new Error(); + } catch (e) { + expect(e).to.be.instanceOf(SoloError); + } + }); + + it('should succeed if component is present', async () => { + const [key, value] = constants.SOLO_HEDERA_EXPLORER_LABEL.split('='); + await createPod(mirrorNodeExplorerName, {[key]: value}); + + // @ts-ignore + await Promise.all(RemoteConfigValidator.validateMirrorNodeExplorers(components, k8)); + }); + }); +}); From 52cd73c06246d1d6f96cbf08e97786f7b634c433 Mon Sep 17 00:00:00 2001 From: instamenta Date: Fri, 6 Dec 2024 10:23:42 +0200 Subject: [PATCH 4/5] fix issues related to merge Signed-off-by: instamenta --- test/e2e/integration/core/remote_config_validator.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/e2e/integration/core/remote_config_validator.test.ts b/test/e2e/integration/core/remote_config_validator.test.ts index 4282d66ee..89de4856d 100644 --- a/test/e2e/integration/core/remote_config_validator.test.ts +++ b/test/e2e/integration/core/remote_config_validator.test.ts @@ -18,9 +18,11 @@ import {it, describe} from 'mocha'; import {expect} from 'chai'; import * as constants from '../../../../src/core/constants.js'; -import {ConfigManager, K8, Templates} from '../../../../src/core/index.js'; +import {ConfigManager} from '../../../../src/core/config_manager.js'; +import {K8} from '../../../../src/core/k8.js'; +import {Templates} from '../../../../src/core/templates.js'; import {testLogger} from '../../../test_util.js'; -import {flags} from '../../../../src/commands/index.js'; +import {Flags as flags} from '../../../../src/commands/flags.js'; import {V1Container, V1ExecAction, V1ObjectMeta, V1Pod, V1PodSpec, V1Probe} from '@kubernetes/client-node'; import {RemoteConfigValidator} from '../../../../src/core/config/remote/remote_config_validator.js'; import {ConsensusNodeStates} from '../../../../src/core/config/remote/enumerations.js'; From 030f338fb5dafe6dfec19430e882c16aeba33cc0 Mon Sep 17 00:00:00 2001 From: instamenta Date: Fri, 6 Dec 2024 13:35:14 +0200 Subject: [PATCH 5/5] lint fix Signed-off-by: instamenta --- src/core/config/remote/remote_config_validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/config/remote/remote_config_validator.ts b/src/core/config/remote/remote_config_validator.ts index 6c4206ec0..9701507e9 100644 --- a/src/core/config/remote/remote_config_validator.ts +++ b/src/core/config/remote/remote_config_validator.ts @@ -19,7 +19,7 @@ import {SoloError} from '../../errors.js'; import type {K8} from '../../k8.js'; import type {ComponentsDataWrapper} from './components_data_wrapper.js'; -import { BaseComponent } from './components/base_component.js'; +import {type BaseComponent} from './components/base_component.js'; /** * Static class is used to validate that components in the remote config