Skip to content

Commit a7bbae0

Browse files
authored
feat: Validate a remote config (#922)
Signed-off-by: instamenta <[email protected]>
1 parent 9cd610d commit a7bbae0

File tree

8 files changed

+398
-14
lines changed

8 files changed

+398
-14
lines changed

src/commands/mirror_node.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export class MirrorNodeCommand extends BaseCommand {
423423
);
424424
},
425425
},
426-
this.addMirrorNodeAndMirrorNodeExplorer(),
426+
this.addMirrorNodeComponents(),
427427
],
428428
{
429429
concurrent: false,
@@ -520,7 +520,7 @@ export class MirrorNodeCommand extends BaseCommand {
520520
},
521521
skip: ctx => !ctx.config.isChartInstalled,
522522
},
523-
this.removeMirrorNodeAndMirrorNodeExplorer(),
523+
this.removeMirrorNodeComponents(),
524524
],
525525
{
526526
concurrent: false,
@@ -595,7 +595,7 @@ export class MirrorNodeCommand extends BaseCommand {
595595
}
596596

597597
/** Removes the mirror node and mirror node explorer components from remote config. */
598-
public removeMirrorNodeAndMirrorNodeExplorer(): ListrTask<any, any, any> {
598+
public removeMirrorNodeComponents(): ListrTask<any, any, any> {
599599
return {
600600
title: 'Remove mirror node and mirror node explorer from remote config',
601601
skip: (): boolean => !this.remoteConfigManager.isLoaded(),
@@ -610,7 +610,7 @@ export class MirrorNodeCommand extends BaseCommand {
610610
}
611611

612612
/** Adds the mirror node and mirror node explorer components to remote config. */
613-
public addMirrorNodeAndMirrorNodeExplorer(): ListrTask<any, any, any> {
613+
public addMirrorNodeComponents(): ListrTask<any, any, any> {
614614
return {
615615
title: 'Add mirror node and mirror node explorer to remote config',
616616
skip: (): boolean => !this.remoteConfigManager.isLoaded(),

src/core/config/remote/components_data_wrapper.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ export class ComponentsDataWrapper implements Validate, ToObject<ComponentsDataS
5959
* @param mirrorNodeExplorers - Mirror Node Explorers record mapping service name to mirror node explorers components
6060
*/
6161
private constructor(
62-
private readonly relays: Record<ComponentName, RelayComponent> = {},
63-
private readonly haProxies: Record<ComponentName, HaProxyComponent> = {},
64-
private readonly mirrorNodes: Record<ComponentName, MirrorNodeComponent> = {},
65-
private readonly envoyProxies: Record<ComponentName, EnvoyProxyComponent> = {},
66-
private readonly consensusNodes: Record<ComponentName, ConsensusNodeComponent> = {},
67-
private readonly mirrorNodeExplorers: Record<ComponentName, MirrorNodeExplorerComponent> = {},
62+
public readonly relays: Record<ComponentName, RelayComponent> = {},
63+
public readonly haProxies: Record<ComponentName, HaProxyComponent> = {},
64+
public readonly mirrorNodes: Record<ComponentName, MirrorNodeComponent> = {},
65+
public readonly envoyProxies: Record<ComponentName, EnvoyProxyComponent> = {},
66+
public readonly consensusNodes: Record<ComponentName, ConsensusNodeComponent> = {},
67+
public readonly mirrorNodeExplorers: Record<ComponentName, MirrorNodeExplorerComponent> = {},
6868
) {
6969
this.validate();
7070
}

src/core/config/remote/remote_config_manager.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ import {RemoteConfigMetadata} from './metadata.js';
2222
import {Flags as flags} from '../../../commands/flags.js';
2323
import * as yaml from 'yaml';
2424
import {ComponentsDataWrapper} from './components_data_wrapper.js';
25+
import {RemoteConfigValidator} from './remote_config_validator.js';
2526
import type {K8} from '../../k8.js';
2627
import type {Cluster, Namespace} from './types.js';
2728
import type {SoloLogger} from '../../logging.js';
28-
import type {ListrTask} from 'listr2';
2929
import type {ConfigManager} from '../../config_manager.js';
3030
import type {LocalConfig} from '../local_config.js';
3131
import type {DeploymentStructure} from '../local_config_data.js';
3232
import {type ContextClusterStructure} from '../../../types/config_types.js';
33-
import {type Optional} from '../../../types/index.js';
33+
import {type EmptyContextConfig, type Optional, type SoloListrTask} from '../../../types/index.js';
3434
import type * as k8s from '@kubernetes/client-node';
3535

3636
interface ListrContext {
@@ -138,6 +138,7 @@ export class RemoteConfigManager {
138138
if (!configMap) return false;
139139

140140
this.remoteConfig = RemoteConfigDataWrapper.fromConfigmap(configMap);
141+
141142
return true;
142143
}
143144

@@ -150,7 +151,7 @@ export class RemoteConfigManager {
150151
* @param argv - arguments containing command input for historical reference.
151152
* @returns a Listr task which loads the remote configuration.
152153
*/
153-
public buildLoadTask(argv: {_: string[]}): ListrTask {
154+
public buildLoadTask(argv: {_: string[]}): SoloListrTask<EmptyContextConfig> {
154155
const self = this;
155156

156157
return {
@@ -171,6 +172,8 @@ export class RemoteConfigManager {
171172
// throw new SoloError('Failed to load remote config')
172173
}
173174

175+
await RemoteConfigValidator.validateComponents(self.remoteConfig.components, self.k8);
176+
174177
const currentCommand = argv._.join(' ');
175178
self.remoteConfig!.addCommandToHistory(currentCommand);
176179

@@ -185,7 +188,7 @@ export class RemoteConfigManager {
185188
*
186189
* @returns a Listr task which creates the remote configuration.
187190
*/
188-
public buildCreateTask(): ListrTask<ListrContext> {
191+
public buildCreateTask(): SoloListrTask<ListrContext> {
189192
const self = this;
190193

191194
return {
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* Copyright (C) 2024 Hedera Hashgraph, LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the ""License"");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an ""AS IS"" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
import * as constants from '../../constants.js';
18+
import {SoloError} from '../../errors.js';
19+
20+
import type {K8} from '../../k8.js';
21+
import type {ComponentsDataWrapper} from './components_data_wrapper.js';
22+
import {type BaseComponent} from './components/base_component.js';
23+
24+
/**
25+
* Static class is used to validate that components in the remote config
26+
* are present in the kubernetes cluster, and throw errors if there is mismatch.
27+
*/
28+
export class RemoteConfigValidator {
29+
/**
30+
* Gathers together and handles validation of all components.
31+
*
32+
* @param components - components which to validate.
33+
* @param k8 - to validate the elements.
34+
* TODO: Make compatible with multi-cluster K8 implementation
35+
*/
36+
public static async validateComponents(components: ComponentsDataWrapper, k8: K8): Promise<void> {
37+
await Promise.all([
38+
...RemoteConfigValidator.validateRelays(components, k8),
39+
...RemoteConfigValidator.validateHaProxies(components, k8),
40+
...RemoteConfigValidator.validateMirrorNodes(components, k8),
41+
...RemoteConfigValidator.validateEnvoyProxies(components, k8),
42+
...RemoteConfigValidator.validateConsensusNodes(components, k8),
43+
...RemoteConfigValidator.validateMirrorNodeExplorers(components, k8),
44+
]);
45+
}
46+
47+
private static validateRelays(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
48+
return Object.values(components.relays).map(async component => {
49+
try {
50+
const pods = await k8.getPodsByLabel([constants.SOLO_RELAY_LABEL]);
51+
52+
// to return the generic error message
53+
if (!pods.length) throw new Error('Pod not found');
54+
} catch (e) {
55+
RemoteConfigValidator.throwValidationError('Relay', component, e);
56+
}
57+
});
58+
}
59+
60+
private static validateHaProxies(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
61+
return Object.values(components.haProxies).map(async component => {
62+
try {
63+
const pod = await k8.getPodByName(component.name);
64+
65+
// to return the generic error message
66+
if (!pod) throw new Error('Pod not found');
67+
} catch (e) {
68+
RemoteConfigValidator.throwValidationError('HaProxy', component, e);
69+
}
70+
});
71+
}
72+
73+
private static validateMirrorNodes(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
74+
return Object.values(components.mirrorNodes).map(async component => {
75+
try {
76+
const pods = await k8.getPodsByLabel(constants.SOLO_HEDERA_MIRROR_IMPORTER);
77+
78+
// to return the generic error message
79+
if (!pods.length) throw new Error('Pod not found');
80+
} catch (e) {
81+
RemoteConfigValidator.throwValidationError('Mirror node', component, e);
82+
}
83+
});
84+
}
85+
86+
private static validateEnvoyProxies(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
87+
return Object.values(components.envoyProxies).map(async component => {
88+
try {
89+
const pod = await k8.getPodByName(component.name);
90+
91+
// to return the generic error message
92+
if (!pod) throw new Error('Pod not found');
93+
} catch (e) {
94+
RemoteConfigValidator.throwValidationError('Envoy proxy', component, e);
95+
}
96+
});
97+
}
98+
99+
private static validateConsensusNodes(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
100+
return Object.values(components.consensusNodes).map(async component => {
101+
try {
102+
const pod = await k8.getPodByName(component.name);
103+
104+
// to return the generic error message
105+
if (!pod) throw new Error('Pod not found');
106+
} catch (e) {
107+
RemoteConfigValidator.throwValidationError('Consensus node', component, e);
108+
}
109+
});
110+
}
111+
112+
private static validateMirrorNodeExplorers(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
113+
return Object.values(components.mirrorNodeExplorers).map(async component => {
114+
try {
115+
const pods = await k8.getPodsByLabel([constants.SOLO_HEDERA_EXPLORER_LABEL]);
116+
117+
// to return the generic error message
118+
if (!pods.length) throw new Error('Pod not found');
119+
} catch (e) {
120+
RemoteConfigValidator.throwValidationError('Mirror node explorer', component, e);
121+
}
122+
});
123+
}
124+
125+
/**
126+
* Generic handler that throws errors.
127+
*
128+
* @param type - name to display in error message
129+
* @param component - component which is not found in the cluster
130+
* @param e - original error for the kube client
131+
*/
132+
private static throwValidationError(type: string, component: BaseComponent, e: Error | unknown): never {
133+
throw new SoloError(
134+
`${type} in remote config with name ${component.name} ` +
135+
`was not found in namespace: ${component.namespace}, cluster: ${component.cluster}`,
136+
e,
137+
{component: component.toObject()},
138+
);
139+
}
140+
}

src/core/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ export const MIRROR_NODE_CHART = 'hedera-mirror';
6464
export const MIRROR_NODE_RELEASE_NAME = 'mirror';
6565
export const HEDERA_EXPLORER_CHART_UTL = 'oci://ghcr.io/hashgraph/hedera-mirror-node-explorer/hedera-explorer';
6666
export const HEDERA_EXPLORER_CHART = 'hedera-explorer';
67+
export const SOLO_RELAY_LABEL = 'app=hedera-json-rpc-relay';
68+
export const SOLO_HEDERA_EXPLORER_LABEL = 'app.kubernetes.io/name=hedera-explorer';
69+
70+
export const SOLO_HEDERA_MIRROR_IMPORTER = [
71+
'app.kubernetes.io/component=importer',
72+
'app.kubernetes.io/instance=mirror',
73+
];
6774

6875
export const DEFAULT_CHART_REPO: Map<string, string> = new Map()
6976
.set(JSON_RPC_RELAY_CHART, JSON_RPC_RELAY_CHART_URL)

src/core/templates.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,12 @@ export class Templates {
255255
static parseClusterAliases(clusters: string) {
256256
return clusters ? clusters.split(',') : [];
257257
}
258+
259+
public static renderEnvoyProxyName(nodeAlias: NodeAlias): string {
260+
return `envoy-proxy-${nodeAlias}`;
261+
}
262+
263+
public static renderHaProxyName(nodeAlias: NodeAlias): string {
264+
return `haproxy-${nodeAlias}`;
265+
}
258266
}

src/types/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type * as x509 from '@peculiar/x509';
1818
import type net from 'net';
1919
import type * as WebSocket from 'ws';
2020
import type crypto from 'crypto';
21+
import type {ListrTask} from 'listr2';
2122

2223
// NOTE: DO NOT add any Solo imports in this file to avoid circular dependencies
2324

@@ -75,3 +76,7 @@ export interface ToObject<T> {
7576
*/
7677
toObject(): T;
7778
}
79+
80+
export type SoloListrTask<T> = ListrTask<T, any, any>;
81+
82+
export type EmptyContextConfig = object;

0 commit comments

Comments
 (0)