@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
214 lines (193 loc) • 8.4 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {inject, injectable} from 'tsyringe-neo';
import {patchInject} from '../../dependency-injection/container-helper.js';
import {InjectTokens} from '../../dependency-injection/inject-tokens.js';
import {SoloError} from '../../errors/solo-error.js';
import {Templates} from '../../templates.js';
import {RemoteConfigValidatorApi} from './api/remote-config-validator-api.js';
import {DeploymentStateSchema} from '../../../data/schema/model/remote/deployment-state-schema.js';
import {DeploymentPhase} from '../../../data/schema/model/remote/deployment-phase.js';
import {NamespaceName} from '../../../types/namespace/namespace-name.js';
import {type BaseStateSchema} from '../../../data/schema/model/remote/state/base-state-schema.js';
import {type LocalConfigRuntimeState} from '../../../business/runtime-state/config/local/local-config-runtime-state.js';
import {type ConsensusNodeStateSchema} from '../../../data/schema/model/remote/state/consensus-node-state-schema.js';
import {type ChartManager} from '../../chart-manager.js';
import {type Pod} from '../../../integration/kube/resources/pod/pod.js';
import {type ComponentId, type Context} from '../../../types/index.js';
import {type K8Factory} from '../../../integration/kube/k8-factory.js';
import * as constants from '../../constants.js';
import {NodeAlias, NodeAliases} from '../../../types/aliases.js';
import {RelayNodeStateSchema} from '../../../data/schema/model/remote/state/relay-node-state-schema.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 implements RemoteConfigValidatorApi {
public constructor(
(InjectTokens.K8Factory) private readonly k8Factory?: K8Factory,
(InjectTokens.LocalConfigRuntimeState) private readonly localConfig?: LocalConfigRuntimeState,
(InjectTokens.ChartManager) private readonly chartManager?: ChartManager,
) {
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name);
this.chartManager = patchInject(chartManager, InjectTokens.ChartManager, this.constructor.name);
}
private static consensusNodeSkipConditionCallback(nodeComponent: ConsensusNodeStateSchema): boolean {
return (
nodeComponent.metadata.phase === DeploymentPhase.REQUESTED ||
nodeComponent.metadata.phase === DeploymentPhase.STOPPED
);
}
// This skips components that are requested, because they are not yet deployed.
// This is needed to avoid errors during the deployment of the requested components.
// Especially during one-shot deployments.
private static componentSkipConditionCallback(component: BaseStateSchema): boolean {
return component.metadata.phase === DeploymentPhase.REQUESTED;
}
public static componentValidationsMapping: Record<
string,
{
getLabelsCallback: (id: ComponentId, legacyReleaseName?: string) => string[];
displayName: string;
skipCondition?: (component: BaseStateSchema) => boolean;
legacyReleaseName?: string;
}
> = {
relayNodes: {
displayName: 'Relay Nodes',
getLabelsCallback: Templates.renderRelayLabels,
legacyReleaseName: constants.JSON_RPC_RELAY_RELEASE_NAME,
skipCondition: RemoteConfigValidator.componentSkipConditionCallback,
},
haProxies: {
displayName: 'HaProxy',
getLabelsCallback: Templates.renderHaProxyLabels,
},
mirrorNodes: {
displayName: 'Mirror Node',
getLabelsCallback: Templates.renderMirrorNodeLabels,
legacyReleaseName: constants.MIRROR_NODE_RELEASE_NAME,
skipCondition: RemoteConfigValidator.componentSkipConditionCallback,
},
envoyProxies: {
displayName: 'Envoy Proxy',
getLabelsCallback: Templates.renderEnvoyProxyLabels,
},
explorers: {
displayName: 'Explorer',
getLabelsCallback: Templates.renderExplorerLabels,
legacyReleaseName: 'hiero-explorer',
skipCondition: RemoteConfigValidator.componentSkipConditionCallback,
},
consensusNodes: {
displayName: 'Consensus Node',
getLabelsCallback: Templates.renderConsensusNodeLabels,
skipCondition: RemoteConfigValidator.consensusNodeSkipConditionCallback,
},
blockNodes: {
displayName: 'Block Node',
getLabelsCallback: Templates.renderBlockNodeLabels,
legacyReleaseName: `${constants.BLOCK_NODE_RELEASE_NAME}-0`,
skipCondition: RemoteConfigValidator.componentSkipConditionCallback,
},
};
public async validateComponents(
namespace: NamespaceName,
skipConsensusNodes: boolean,
state: Readonly<DeploymentStateSchema>,
): Promise<void> {
const validationPromises: Promise<void>[] = Object.entries(RemoteConfigValidator.componentValidationsMapping)
.filter(([key]): boolean => key !== 'consensusNodes' || !skipConsensusNodes)
.flatMap(([key, {getLabelsCallback, displayName, skipCondition, legacyReleaseName}]): Promise<void>[] =>
this.validateComponentGroup(
key,
namespace,
state[key],
getLabelsCallback,
displayName,
skipCondition,
legacyReleaseName,
),
);
await Promise.all(validationPromises);
}
private validateComponentGroup(
key: string,
namespace: NamespaceName,
components: BaseStateSchema[],
getLabelsCallback: (id: ComponentId, legacyReleaseName?: string) => string[],
displayName: string,
skipCondition?: (component: BaseStateSchema) => boolean,
legacyReleaseName?: string,
): Promise<void>[] {
return components.map(async (component): Promise<void> => {
if (skipCondition?.(component)) {
return;
}
const context: Context = this.localConfig.configuration.clusterRefs.get(component.metadata.cluster)?.toString();
let useLegacyReleaseName: boolean = false;
if (legacyReleaseName && component.metadata.id <= 1) {
if (key === 'relayNodes') {
const nodeAliases: NodeAliases = (component as RelayNodeStateSchema)?.consensusNodeIds.map(
(nodeId): NodeAlias => Templates.renderNodeAliasFromNumber(nodeId + 1),
);
legacyReleaseName = `${legacyReleaseName}-${nodeAliases.join('-')}`;
}
const isLegacyChartInstalled: boolean = await this.chartManager.isChartInstalled(
namespace,
legacyReleaseName,
context,
);
if (isLegacyChartInstalled) {
useLegacyReleaseName = true;
}
}
const labels: string[] = useLegacyReleaseName
? getLabelsCallback(component.metadata.id, legacyReleaseName)
: getLabelsCallback(component.metadata.id);
try {
const pods: Pod[] = await this.k8Factory.getK8(context).pods().list(namespace, labels);
if (pods.length === 0) {
throw new Error('Pod not found'); // to return the generic error message
}
} catch (error) {
throw RemoteConfigValidator.buildValidationError(displayName, component, error, labels);
}
});
}
/**
* Generic handler that throws errors.
*
* @param displayName - name to display in error message
* @param component - component which is not found in the cluster
* @param error - original error for the kube client
* @param labels - labels used to find the component
*/
private static buildValidationError(
displayName: string,
component: BaseStateSchema,
error: Error | unknown,
labels?: string[],
): SoloError {
return new SoloError(
RemoteConfigValidator.buildValidationErrorMessage(displayName, component, labels),
error,
component,
);
}
public static buildValidationErrorMessage(
displayName: string,
component: BaseStateSchema,
labels: string[] = [],
): string {
let message: string =
`${displayName} in remote config with id ${component.metadata.id} was not found in ` +
`namespace: ${component.metadata.namespace}, ` +
`cluster: ${component.metadata.cluster}`;
if (labels?.length !== 0) {
message += `, labels: ${labels}`;
}
return message;
}
}