Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Validate a remote config #922

8 changes: 4 additions & 4 deletions src/commands/mirror_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ export class MirrorNodeCommand extends BaseCommand {
);
},
},
this.addMirrorNodeAndMirrorNodeExplorer(),
this.addMirrorNodeComponents(),
],
{
concurrent: false,
Expand Down Expand Up @@ -520,7 +520,7 @@ export class MirrorNodeCommand extends BaseCommand {
},
skip: ctx => !ctx.config.isChartInstalled,
},
this.removeMirrorNodeAndMirrorNodeExplorer(),
this.removeMirrorNodeComponents(),
],
{
concurrent: false,
Expand Down Expand Up @@ -595,7 +595,7 @@ export class MirrorNodeCommand extends BaseCommand {
}

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

/** Adds the mirror node and mirror node explorer components to remote config. */
public addMirrorNodeAndMirrorNodeExplorer(): ListrTask<any, any, any> {
public addMirrorNodeComponents(): ListrTask<any, any, any> {
return {
title: 'Add mirror node and mirror node explorer to remote config',
skip: (): boolean => !this.remoteConfigManager.isLoaded(),
Expand Down
12 changes: 6 additions & 6 deletions src/core/config/remote/components_data_wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ export class ComponentsDataWrapper implements Validate, ToObject<ComponentsDataS
* @param mirrorNodeExplorers - Mirror Node Explorers record mapping service name to mirror node explorers components
*/
private constructor(
private readonly relays: Record<ComponentName, RelayComponent> = {},
private readonly haProxies: Record<ComponentName, HaProxyComponent> = {},
private readonly mirrorNodes: Record<ComponentName, MirrorNodeComponent> = {},
private readonly envoyProxies: Record<ComponentName, EnvoyProxyComponent> = {},
private readonly consensusNodes: Record<ComponentName, ConsensusNodeComponent> = {},
private readonly mirrorNodeExplorers: Record<ComponentName, MirrorNodeExplorerComponent> = {},
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();
}
Expand Down
11 changes: 7 additions & 4 deletions src/core/config/remote/remote_config_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
import {Flags as flags} from '../../../commands/flags.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} from '../../../types/config_types.js';
import {type Optional} from '../../../types/index.js';
import {type EmptyContextConfig, type Optional, type SoloListrTask} from '../../../types/index.js';
import type * as k8s from '@kubernetes/client-node';

interface ListrContext {
Expand Down Expand Up @@ -138,6 +138,7 @@
if (!configMap) return false;

this.remoteConfig = RemoteConfigDataWrapper.fromConfigmap(configMap);

Check warning on line 141 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L141

Added line #L141 was not covered by tests
return true;
}

Expand All @@ -150,7 +151,7 @@
* @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<EmptyContextConfig> {
const self = this;

return {
Expand All @@ -171,6 +172,8 @@
// throw new SoloError('Failed to load remote config')
}

await RemoteConfigValidator.validateComponents(self.remoteConfig.components, self.k8);

Check warning on line 176 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L176

Added line #L176 was not covered by tests
const currentCommand = argv._.join(' ');
self.remoteConfig!.addCommandToHistory(currentCommand);

Expand All @@ -185,7 +188,7 @@
*
* @returns a Listr task which creates the remote configuration.
*/
public buildCreateTask(): ListrTask<ListrContext> {
public buildCreateTask(): SoloListrTask<ListrContext> {
const self = this;

return {
Expand Down
140 changes: 140 additions & 0 deletions src/core/config/remote/remote_config_validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* 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 {SoloError} from '../../errors.js';

import type {K8} from '../../k8.js';
import type {ComponentsDataWrapper} from './components_data_wrapper.js';
import {type BaseComponent} from './components/base_component.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<void> {
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),
]);
}

Check warning on line 45 in src/core/config/remote/remote_config_validator.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_validator.ts#L37-L45

Added lines #L37 - L45 were not covered by tests

private static validateRelays(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
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 validateHaProxies(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
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 validateMirrorNodes(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
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 validateEnvoyProxies(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
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 validateConsensusNodes(components: ComponentsDataWrapper, k8: K8): Promise<void>[] {
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<void>[] {
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);
}
});
}

/**
* 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()},
);
}
}
7 changes: 7 additions & 0 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = new Map()
.set(JSON_RPC_RELAY_CHART, JSON_RPC_RELAY_CHART_URL)
Expand Down
8 changes: 8 additions & 0 deletions src/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}
}
5 changes: 5 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type * as x509 from '@peculiar/x509';
import type net from 'net';
import type * as WebSocket from 'ws';
import type crypto from 'crypto';
import type {ListrTask} from 'listr2';

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

Expand Down Expand Up @@ -75,3 +76,7 @@ export interface ToObject<T> {
*/
toObject(): T;
}

export type SoloListrTask<T> = ListrTask<T, any, any>;

export type EmptyContextConfig = object;
Loading
Loading