Skip to content

Commit

Permalink
feat: Validate a remote config (#922)
Browse files Browse the repository at this point in the history
Signed-off-by: instamenta <[email protected]>
  • Loading branch information
instamenta authored Dec 6, 2024
1 parent 9cd610d commit a7bbae0
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 14 deletions.
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 {RemoteConfigMetadata} from './metadata.js';
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 @@ export class RemoteConfigManager {
if (!configMap) return false;

this.remoteConfig = RemoteConfigDataWrapper.fromConfigmap(configMap);

return true;
}

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

return {
Expand All @@ -171,6 +172,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);

Expand All @@ -185,7 +188,7 @@ export class RemoteConfigManager {
*
* @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),
]);
}

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

0 comments on commit a7bbae0

Please sign in to comment.